# Metadata Discovery Learn how Svrnty.CQRS uses metadata-driven discovery for automatic endpoint generation. ## Overview Svrnty.CQRS uses a **metadata pattern** instead of reflection-heavy assembly scanning. When you register a handler, the framework creates metadata that describes it. Discovery services then query this metadata to generate endpoints. ## Why Metadata Instead of Reflection? ### Traditional Approach (Assembly Scanning) ```csharp // ❌ Slow, not AOT-compatible public void MapEndpoints(IApplicationBuilder app) { var assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var assembly in assemblies) { var handlers = assembly.GetTypes() .Where(t => t.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommandHandler<,>))); foreach (var handler in handlers) { // Create endpoint... } } } ``` **Problems:** - Slow startup (scanning all assemblies) - Not AOT-compatible (heavy reflection) - May find unintended handlers - Hard to control what gets discovered ### Metadata Approach (Svrnty.CQRS) ```csharp // ✅ Fast, AOT-compatible services.AddCommand(); // Behind the scenes: services.AddSingleton( new CommandMeta()); ``` **Benefits:** - Fast startup (no assembly scanning) - AOT-compatible (metadata created at compile time) - Explicit control (only registered handlers discovered) - Type-safe (compile-time checking) ## How It Works ### Step 1: Registration Creates Metadata When you call `AddCommand()` or `AddQuery()`: ```csharp // User code builder.Services.AddCommand(); ``` This extension method does two things: **1. Registers the handler in DI:** ```csharp services.AddScoped, CreateUserCommandHandler>(); ``` **2. Creates and registers metadata:** ```csharp services.AddSingleton(new CommandMeta { CommandType = typeof(CreateUserCommand), HandlerType = typeof(CreateUserCommandHandler), ResultType = typeof(int), CommandName = "createUser" // Auto-generated from type name }); ``` ### Step 2: Discovery Service Enumerates Metadata The discovery service queries all registered metadata: ```csharp public class CommandDiscovery : ICommandDiscovery { private readonly IEnumerable _commandMetas; public CommandDiscovery(IEnumerable commandMetas) { _commandMetas = commandMetas; } public IEnumerable GetCommands() => _commandMetas; public ICommandMeta? FindCommand(string name) { return _commandMetas.FirstOrDefault(m => m.CommandName.Equals(name, StringComparison.OrdinalIgnoreCase)); } public bool CommandExists(Type commandType) { return _commandMetas.Any(m => m.CommandType == commandType); } } ``` ### Step 3: Endpoint Generation Uses Discovery HTTP and gRPC integrations use discovery to create endpoints: ```csharp public static IEndpointRouteBuilder UseSvrntyCqrs(this IEndpointRouteBuilder endpoints) { var commandDiscovery = endpoints.ServiceProvider.GetRequiredService(); // Enumerate all registered commands foreach (var commandMeta in commandDiscovery.GetCommands()) { if (commandMeta.IgnoreEndpoint) continue; // Create endpoint for this command endpoints.MapPost($"/api/command/{commandMeta.CommandName}", async (HttpContext context) => { // Deserialize, validate, execute handler... }); } // Same for queries... } ``` ## Metadata Interfaces ### ICommandMeta Describes a command handler: ```csharp public interface ICommandMeta { Type CommandType { get; } // typeof(CreateUserCommand) Type HandlerType { get; } // typeof(CreateUserCommandHandler) Type? ResultType { get; } // typeof(int) or null string CommandName { get; } // "createUser" bool IgnoreEndpoint { get; } // From [IgnoreCommand] string? CustomName { get; } // From [CommandName("...")] } ``` ### IQueryMeta Describes a query handler: ```csharp public interface IQueryMeta { Type QueryType { get; } // typeof(GetUserQuery) Type HandlerType { get; } // typeof(GetUserQueryHandler) Type ResultType { get; } // typeof(UserDto) string QueryName { get; } // "getUser" bool IgnoreEndpoint { get; } // From [IgnoreQuery] string? CustomName { get; } // From [QueryName("...")] string Category { get; } // "Query" or "DynamicQuery" } ``` ## Discovery Services ### ICommandDiscovery Provides command enumeration and lookup: ```csharp public interface ICommandDiscovery { IEnumerable GetCommands(); ICommandMeta? FindCommand(string name); bool CommandExists(Type commandType); } ``` **Usage:** ```csharp var commandDiscovery = serviceProvider.GetRequiredService(); // List all commands foreach (var meta in commandDiscovery.GetCommands()) { Console.WriteLine($"Command: {meta.CommandName} → {meta.HandlerType.Name}"); } // Find specific command var meta = commandDiscovery.FindCommand("createUser"); if (meta != null) { Console.WriteLine($"Found: {meta.CommandType.Name}"); } ``` ### IQueryDiscovery Provides query enumeration and lookup: ```csharp public interface IQueryDiscovery { IEnumerable GetQueries(); IQueryMeta? FindQuery(string name); bool QueryExists(Type queryType); } ``` **Usage:** ```csharp var queryDiscovery = serviceProvider.GetRequiredService(); // List all queries foreach (var meta in queryDiscovery.GetQueries()) { Console.WriteLine($"Query: {meta.QueryName} → {meta.ResultType.Name}"); } // Find specific query var meta = queryDiscovery.FindQuery("getUser"); ``` ## Naming Conventions Endpoint names are auto-generated from type names: ### Default Naming ```csharp CreateUserCommand → "createUser" UpdateProfileCommand → "updateProfile" DeleteOrderCommand → "deleteOrder" GetUserQuery → "getUser" SearchProductsQuery → "searchProducts" ListOrdersQuery → "listOrders" ``` **Rules:** 1. Remove "Command" or "Query" suffix 2. Convert PascalCase to lowerCamelCase ### Custom Naming Use attributes to override: ```csharp [CommandName("register")] public record CreateUserCommand { /* ... */ } // Endpoint: POST /api/command/register [QueryName("user")] public record GetUserQuery { /* ... */ } // Endpoint: GET /api/query/user ``` ### Ignoring Endpoints Prevent endpoint generation: ```csharp [IgnoreCommand] public record InternalCommand { /* ... */ } // No endpoint created [IgnoreQuery] public record InternalQuery { /* ... */ } // No endpoint created ``` ## Registration Patterns ### Basic Registration ```csharp // Command with result services.AddCommand(); // Command without result services.AddCommand(); // Query (always has result) services.AddQuery(); ``` ### Registration with Validator ```csharp services.AddCommand(); services.AddQuery, SearchUsersQueryHandler, SearchUsersQueryValidator>(); ``` ### Bulk Registration For multiple handlers, use loops: ```csharp // Register all commands in namespace services.AddCommand(); services.AddCommand(); services.AddCommand(); // Or use a helper method public static class CommandRegistration { public static IServiceCollection AddUserCommands(this IServiceCollection services) { services.AddCommand(); services.AddCommand(); services.AddCommand(); return services; } } // Usage builder.Services.AddUserCommands(); ``` ## Type Safety Metadata discovery maintains compile-time type safety: ### Compile-Time Checking ```csharp // ✅ Correct - handler matches command and result services.AddCommand(); // ❌ Compile error - handler doesn't implement ICommandHandler services.AddCommand(); // ❌ Compile error - result type mismatch services.AddCommand(); // (Handler returns int, not string) ``` ### Generic Constraints Registration methods use generic constraints: ```csharp public static IServiceCollection AddCommand( this IServiceCollection services) where THandler : class, ICommandHandler { // Constraint ensures THandler implements correct interface } ``` ## Discovery at Runtime ### Accessing Discovery Services ```csharp var app = builder.Build(); // Get discovery service var commandDiscovery = app.Services.GetRequiredService(); // List all registered commands foreach (var meta in commandDiscovery.GetCommands()) { Console.WriteLine($"Command: {meta.CommandName}"); Console.WriteLine($" Type: {meta.CommandType.Name}"); Console.WriteLine($" Handler: {meta.HandlerType.Name}"); Console.WriteLine($" Result: {meta.ResultType?.Name ?? "void"}"); Console.WriteLine(); } ``` ### Dynamic Endpoint Creation Discovery enables dynamic behavior: ```csharp app.MapGet("/api/commands", (ICommandDiscovery discovery) => { var commands = discovery.GetCommands() .Select(m => new { name = m.CommandName, type = m.CommandType.Name, hasResult = m.ResultType != null }); return Results.Ok(commands); }); // GET /api/commands returns: // [ // { "name": "createUser", "type": "CreateUserCommand", "hasResult": true }, // { "name": "deleteUser", "type": "DeleteUserCommand", "hasResult": false } // ] ``` ## Performance ### Startup Performance **Fast startup:** - No assembly scanning - Metadata created during registration (one-time cost) - Discovery just queries singletons **Benchmark:** ``` Assembly scanning: ~200ms for 100 handlers Metadata pattern: ~5ms for 100 handlers ``` ### Runtime Performance **Fast lookup:** - Metadata stored as singletons - Simple enumeration (no reflection) - LINQ queries over in-memory collection ### Memory Footprint **Minimal memory:** - One metadata object per handler - Metadata is small (just type references and strings) - Singleton lifetime (shared across requests) ## AOT Compatibility Metadata pattern is AOT (Ahead-of-Time) compatible: ```csharp // ✅ AOT-compatible services.AddCommand(); // Metadata types known at compile time // No runtime type discovery // No assembly scanning ``` **Benefits:** - Faster startup - Smaller deployment size - Better performance - Suitable for containers and serverless ## Comparison: Metadata vs Reflection | Aspect | Metadata Pattern | Assembly Scanning | |--------|-----------------|-------------------| | **Startup time** | Fast (~5ms) | Slow (~200ms) | | **AOT compatible** | ✅ Yes | ❌ No | | **Type safety** | ✅ Compile-time | ⚠️ Runtime | | **Explicit control** | ✅ Yes | ❌ No (finds all) | | **Memory usage** | Low | Medium | | **Maintainability** | ✅ Clear registration | ⚠️ "Magic" discovery | ## Best Practices ### ✅ DO - Register handlers explicitly - Use meaningful command/query names - Group related registrations - Create extension methods for bulk registration - Use attributes for customization ([CommandName], [IgnoreCommand]) ### ❌ DON'T - Don't rely on assembly scanning - Don't use reflection to find handlers - Don't duplicate registrations - Don't forget to register handlers (they won't be discovered) ## Troubleshooting ### Handler Not Found **Problem:** Command/query endpoint returns 404 **Cause:** Handler not registered **Solution:** ```csharp // Ensure you registered the handler builder.Services.AddCommand(); // Verify registration at startup var discovery = app.Services.GetRequiredService(); var found = discovery.FindCommand("yourCommand"); if (found == null) { Console.WriteLine("ERROR: YourCommand not registered!"); } ``` ### Wrong Handler Invoked **Problem:** Different handler executes than expected **Cause:** Multiple handlers for same command **Solution:** ```csharp // ❌ Don't register multiple handlers for same command services.AddCommand(); services.AddCommand(); // Last one wins // ✅ Only register one handler per command services.AddCommand(); ``` ## What's Next? - **[Modular Solution Structure](modular-solution-structure.md)** - Organize large applications - **[Dependency Injection](dependency-injection.md)** - DI patterns for handlers - **[Extensibility Points](extensibility-points.md)** - Customization mechanisms ## See Also - [Core Features: Commands](../core-features/commands/README.md) - Command documentation - [Core Features: Queries](../core-features/queries/README.md) - Query documentation - [HTTP Integration](../http-integration/README.md) - How HTTP endpoints are generated - [gRPC Integration](../grpc-integration/README.md) - How gRPC services are generated