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

6.2 KiB

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

public record GetUserQuery { }
// Endpoints:
//   GET  /api/query/getUser?userId=123
//   POST /api/query/getUser (with JSON body)

Custom Name

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

[QueryName("products/{id}")]
public record GetProductQuery
{
    public int Id { get; init; }
}

// Endpoint: GET /api/query/products/{id}

[IgnoreQuery]

Prevent endpoint generation for internal queries.

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

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).

[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:

[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

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

[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

[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

[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

[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