dotnet-cqrs/docs/core-features/queries/query-attributes.md

292 lines
6.2 KiB
Markdown

# 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<InternalReportQuery, ReportDto> _queryHandler;
public async Task<ReportDto> 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<CacheableQueryAttribute>();
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<GenerateDailyStatisticsQuery, StatisticsDto> _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<DownloadInvoiceQuery, FileResult>
{
public async Task<FileResult> 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)