292 lines
6.2 KiB
Markdown
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)
|