# Query Attributes Control query behavior using attributes. ## Overview Attributes customize how queries are discovered, named, and exposed as endpoints. ## [QueryName] Override the default endpoint name. ### Default Naming ```csharp public record GetUserQuery { } // Endpoints: // GET /api/query/getUser?userId=123 // POST /api/query/getUser (with JSON body) ``` ### Custom Name ```csharp using Svrnty.CQRS.Abstractions; [QueryName("users/search")] public record SearchUsersQuery { public string Keyword { get; init; } = string.Empty; public int Page { get; init; } = 1; public int PageSize { get; init; } = 10; } // Endpoints: // GET /api/query/users/search?keyword=john&page=1&pageSize=10 // POST /api/query/users/search (with JSON body) ``` ### REST-Style Naming ```csharp [QueryName("products/{id}")] public record GetProductQuery { public int Id { get; init; } } // Endpoint: GET /api/query/products/{id} ``` ## [IgnoreQuery] Prevent endpoint generation for internal queries. ```csharp using Svrnty.CQRS.Abstractions; [IgnoreQuery] public record InternalReportQuery { public DateTime StartDate { get; init; } public DateTime EndDate { get; init; } } // No endpoint created - internal use only ``` **Use cases:** - Internal queries called from code - Background job queries - System queries - Scheduled report queries - Health check queries ### Calling Internal Queries ```csharp public class ReportGenerationService { private readonly IQueryHandler _queryHandler; public async Task GenerateReportAsync(DateTime start, DateTime end) { // Call internal query directly var query = new InternalReportQuery { StartDate = start, EndDate = end }; return await _queryHandler.HandleAsync(query); } } ``` ## [GrpcIgnore] Skip gRPC service generation (HTTP only). ```csharp [GrpcIgnore] public record DownloadFileQuery { public string FileId { get; init; } = string.Empty; } // HTTP: GET /api/query/downloadFile?fileId=abc123 // gRPC: Not generated ``` **Use cases:** - File download queries - Large binary responses - Browser-specific queries - Queries with streaming responses ## Custom Attributes Create your own attributes for metadata: ```csharp [AttributeUsage(AttributeTargets.Class)] public class CacheableQueryAttribute : Attribute { public int DurationSeconds { get; set; } } [AttributeUsage(AttributeTargets.Class)] public class RateLimitedAttribute : Attribute { public int MaxRequestsPerMinute { get; set; } } // Usage [CacheableQuery(DurationSeconds = 300)] [RateLimited(MaxRequestsPerMinute = 100)] public record GetProductListQuery { public string Category { get; init; } = string.Empty; } ``` ### Using Custom Attributes in Middleware ```csharp public class CachingMiddleware { public async Task InvokeAsync(HttpContext context, RequestDelegate next) { var endpoint = context.GetEndpoint(); var cacheAttribute = endpoint?.Metadata .GetMetadata(); if (cacheAttribute != null) { var cacheKey = GenerateCacheKey(context.Request); var cached = await _cache.GetAsync(cacheKey); if (cached != null) { context.Response.ContentType = "application/json"; await context.Response.WriteAsync(cached); return; } // Cache miss - execute query and cache result await next(context); } else { await next(context); } } } ``` ## Attribute Combinations ```csharp [QueryName("reports/sales")] [CacheableQuery(DurationSeconds = 600)] public record GetSalesReportQuery { public DateTime StartDate { get; init; } public DateTime EndDate { get; init; } } [IgnoreQuery] [GrpcIgnore] public record InternalMaintenanceQuery { } ``` ## Best Practices ### ✅ DO - Use [QueryName] for clearer APIs - Use [IgnoreQuery] for internal queries - Document why queries are ignored - Keep custom attributes simple - Use descriptive custom attribute names - Consider caching for expensive queries - Rate limit public queries ### ❌ DON'T - Don't overuse custom naming - Don't create too many custom attributes - Don't put logic in attributes - Don't ignore queries that should be public - Don't skip authorization for internal queries ## Examples ### Public API Query ```csharp [QueryName("products/search")] [CacheableQuery(DurationSeconds = 120)] [RateLimited(MaxRequestsPerMinute = 1000)] public record SearchProductsQuery { public string Keyword { get; init; } = string.Empty; public decimal? MinPrice { get; init; } public decimal? MaxPrice { get; init; } } ``` ### Internal Background Query ```csharp [IgnoreQuery] public record GenerateDailyStatisticsQuery { public DateTime Date { get; init; } } public class DailyStatisticsJob { private readonly IQueryHandler _handler; public async Task RunAsync() { var query = new GenerateDailyStatisticsQuery { Date = DateTime.UtcNow.Date.AddDays(-1) }; var stats = await _handler.HandleAsync(query); await SaveStatisticsAsync(stats); } } ``` ### HTTP-Only File Download ```csharp [GrpcIgnore] [QueryName("files/download")] public record DownloadInvoiceQuery { public int InvoiceId { get; init; } } public class DownloadInvoiceQueryHandler : IQueryHandler { public async Task HandleAsync(DownloadInvoiceQuery query, CancellationToken cancellationToken) { var invoice = await _repository.GetByIdAsync(query.InvoiceId, cancellationToken); if (invoice == null) throw new KeyNotFoundException($"Invoice {query.InvoiceId} not found"); var pdfBytes = await GeneratePdfAsync(invoice); return new FileResult { Content = pdfBytes, FileName = $"invoice-{invoice.Id}.pdf", ContentType = "application/pdf" }; } } ``` ## See Also - [Query Registration](query-registration.md) - [Command Attributes](../commands/command-attributes.md) - [Metadata Discovery](../../architecture/metadata-discovery.md)