Web Api

Фільтри у Web API контексті

Action Filters, Exception Filters, Result Filters для API. Централізована валідація DTO, response wrapping (envelope pattern), correlation IDs, API key authentication та performance monitoring.

Фільтри у Web API контексті

Вступ: Cross-Cutting Concerns

Уявіть, що ви будуєте API з 50 endpoints. Кожен endpoint потребує:

  • Логування запитів та відповідей
  • Валідації вхідних даних
  • Авторизації через API-ключ
  • Вимірювання часу виконання
  • Додавання correlation ID до кожної відповіді
  • Обгортання відповідей у стандартний envelope

Якщо писати цю логіку в кожному контролері, ви отримаєте:

[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(int id)
{
    // 1. Логування
    _logger.LogInformation("Getting product {Id}", id);
    
    // 2. Валідація
    if (id <= 0) return BadRequest("Invalid ID");
    
    // 3. Перевірка API-ключа
    if (!ValidateApiKey()) return Unauthorized();
    
    // 4. Вимірювання часу
    var stopwatch = Stopwatch.StartNew();
    
    // 5. Бізнес-логіка (нарешті!)
    var product = await _db.Products.FindAsync(id);
    
    // 6. Логування результату
    stopwatch.Stop();
    _logger.LogInformation("Request completed in {Ms}ms", stopwatch.ElapsedMilliseconds);
    
    // 7. Обгортання відповіді
    return Ok(new { success = true, data = product });
}

Проблеми цього підходу:

  • ❌ Дублювання коду у кожному методі
  • ❌ Бізнес-логіка захаращена технічними деталями
  • ❌ Складно підтримувати консистентність
  • ❌ Важко тестувати

РішенняFilters (фільтри) — механізм ASP.NET Core для реалізації cross-cutting concerns (наскрізних аспектів) без дублювання коду. Фільтри виконуються автоматично до або після action методів, дозволяючи централізувати логіку.

Передумови: Ця стаття базується на знаннях з попередніх статей (01-05 Web API Controllers), а також на розумінні Filter Pipeline з курсу MVC (стаття 07).

Що ви створите в цій статті

Ми побудуємо E-commerce API з професійною системою фільтрів:

1. ApiKeyAuthFilter — авторизація через API-ключ:

GET /api/products
X-Api-Key: sk_live_abc123xyz
→ 200 OK (якщо ключ валідний)
→ 401 Unauthorized (якщо ключ невалідний)

2. RequestValidationFilter — централізована валідація:

POST /api/products
{ "name": "", "price": -10 }
→ 400 Bad Request з деталями помилок

3. ResponseWrapperFilter — envelope pattern:

{
  "success": true,
  "data": { "id": 1, "name": "Laptop" },
  "meta": {
    "timestamp": "2024-01-15T10:30:00Z",
    "version": "1.0"
  }
}

4. CorrelationIdFilter — traceability:

Response Headers:
X-Correlation-ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890

5. PerformanceMonitoringFilter — метрики:

[INFO] GET /api/products/1 completed in 45ms

До кінця статті ви зможете:

  • Створювати кастомні фільтри для API
  • Використовувати різні типи фільтрів (Action, Exception, Result)
  • Реалізовувати envelope pattern
  • Додавати correlation IDs
  • Вимірювати продуктивність endpoints

Фундаментальні концепції: Типи фільтрів та Pipeline

Filter Pipeline: Порядок виконання

Фільтри виконуються у чіткому порядку навколо action методу:

Loading diagram...
sequenceDiagram
    participant Request
    participant Authorization
    participant Resource
    participant Action
    participant Exception
    participant Result
    participant Response
    
    Request->>Authorization: 1. Authorization Filter
    Authorization->>Resource: 2. Resource Filter (Before)
    Resource->>Action: 3. Action Filter (Before)
    Action->>Action: 4. Action Method Execution
    Action->>Exception: 5. Exception Filter (if error)
    Action->>Action: 6. Action Filter (After)
    Action->>Result: 7. Result Filter (Before)
    Result->>Result: 8. Result Execution
    Result->>Result: 9. Result Filter (After)
    Result->>Resource: 10. Resource Filter (After)
    Resource->>Response: Response
    
    style Request fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style Authorization fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Action fill:#10b981,stroke:#059669,color:#ffffff
    style Exception fill:#ef4444,stroke:#b91c1c,color:#ffffff
    style Result fill:#8b5cf6,stroke:#6d28d9,color:#ffffff
    style Response fill:#3b82f6,stroke:#1d4ed8,color:#ffffff

Типи фільтрів для API

Тип фільтруІнтерфейсКоли виконуєтьсяВикористання для API
AuthorizationIAuthorizationFilterПерший у pipelineAPI key validation, JWT verification
ResourceIResourceFilterДо model bindingCaching, rate limiting
ActionIActionFilterДо/після actionВалідація DTO, логування
ExceptionIExceptionFilterПри виняткуПеретворення у ProblemDetails
ResultIResultFilterДо/після resultResponse wrapping, headers
Для API найчастіше використовуються:
  • Action Filters — валідація, логування
  • Result Filters — обгортання відповідей, додавання headers
  • Authorization Filters — API key, custom auth

Синхронні vs Асинхронні фільтри

Кожен тип фільтру має дві версії:

Синхронний:

public class MyActionFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // До виконання action
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Після виконання action
    }
}

Асинхронний (рекомендовано для API):

public class MyAsyncActionFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        // До виконання action
        
        var resultContext = await next(); // Виконуємо action
        
        // Після виконання action
    }
}
Важливо: Не можна реалізовувати обидва інтерфейси одночасно. Якщо реалізовано IAsyncActionFilter, синхронні методи IActionFilterігноруються.

Практична реалізація: E-commerce API з фільтрами

Крок 1: Налаштування проєкту

Створення проєкту

bash
$ dotnet new webapi -n EcommerceFiltersApi
The template "ASP.NET Core Web API" was created successfully.
$ cd EcommerceFiltersApi
$ dotnet add package Microsoft.EntityFrameworkCore.InMemory
info : PackageReference added successfully

Створення базових моделей

Створіть файл Models/Product.cs:

using System.ComponentModel.DataAnnotations;

namespace EcommerceFiltersApi.Models;

public class Product
{
    public int Id { get; set; }
    
    [Required]
    [MaxLength(200)]
    public required string Name { get; set; }
    
    [Range(0.01, 1_000_000)]
    public decimal Price { get; set; }
    
    [Range(0, int.MaxValue)]
    public int Stock { get; set; }
    
    public bool IsActive { get; set; } = true;
}

public record CreateProductDto
{
    [Required(ErrorMessage = "Product name is required")]
    [MaxLength(200, ErrorMessage = "Name cannot exceed 200 characters")]
    public required string Name { get; init; }

    [Range(0.01, 1_000_000, ErrorMessage = "Price must be between 0.01 and 1,000,000")]
    public decimal Price { get; init; }

    [Range(0, int.MaxValue, ErrorMessage = "Stock cannot be negative")]
    public int Stock { get; init; }
}

Крок 2: Створення фільтрів

1. ApiKeyAuthFilter — Авторизація через API-ключ

Створіть файл Filters/ApiKeyAuthFilter.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace EcommerceFiltersApi.Filters;

public class ApiKeyAuthFilter : IAsyncAuthorizationFilter
{
    private const string ApiKeyHeaderName = "X-Api-Key";
    private readonly IConfiguration _configuration;
    private readonly ILogger<ApiKeyAuthFilter> _logger;

    public ApiKeyAuthFilter(IConfiguration configuration, ILogger<ApiKeyAuthFilter> logger)
    {
        _configuration = configuration;
        _logger = logger;
    }

    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        // Перевіряємо наявність API-ключа у заголовку
        if (!context.HttpContext.Request.Headers.TryGetValue(ApiKeyHeaderName, out var extractedApiKey))
        {
            _logger.LogWarning("API key is missing in request to {Path}", context.HttpContext.Request.Path);
            
            context.Result = new UnauthorizedObjectResult(new ProblemDetails
            {
                Status = StatusCodes.Status401Unauthorized,
                Title = "API Key Required",
                Detail = $"API key must be provided in {ApiKeyHeaderName} header",
                Instance = context.HttpContext.Request.Path
            });
            return;
        }

        // Отримуємо валідні ключі з конфігурації
        var validApiKeys = _configuration.GetSection("ApiKeys").Get<string[]>() ?? Array.Empty<string>();

        // Перевіряємо валідність ключа
        if (!validApiKeys.Contains(extractedApiKey.ToString()))
        {
            _logger.LogWarning("Invalid API key attempted: {ApiKey}", extractedApiKey);
            
            context.Result = new UnauthorizedObjectResult(new ProblemDetails
            {
                Status = StatusCodes.Status401Unauthorized,
                Title = "Invalid API Key",
                Detail = "The provided API key is not valid",
                Instance = context.HttpContext.Request.Path
            });
            return;
        }

        _logger.LogInformation("API key validated successfully for {Path}", context.HttpContext.Request.Path);
        
        // Додаємо інформацію про API key до HttpContext для використання в контролерах
        context.HttpContext.Items["ApiKey"] = extractedApiKey.ToString();
        
        await Task.CompletedTask;
    }
}

Декомпозиція:

  1. IAsyncAuthorizationFilter — виконується першим у pipeline
  2. Перевірка наявності — чи є заголовок X-Api-Key
  3. Валідація ключа — порівняння з конфігурацією
  4. context.Result — якщо встановлено, action не виконується
  5. Логування — всі спроби авторизації логуються
  6. HttpContext.Items — зберігаємо ключ для використання в контролерах

2. RequestValidationFilter — Централізована валідація

Створіть файл Filters/RequestValidationFilter.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace EcommerceFiltersApi.Filters;

public class RequestValidationFilter : IAsyncActionFilter
{
    private readonly ILogger<RequestValidationFilter> _logger;

    public RequestValidationFilter(ILogger<RequestValidationFilter> logger)
    {
        _logger = logger;
    }

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        // Перевіряємо ModelState (DataAnnotations валідація)
        if (!context.ModelState.IsValid)
        {
            _logger.LogWarning(
                "Validation failed for {Action}. Errors: {Errors}",
                context.ActionDescriptor.DisplayName,
                string.Join(", ", context.ModelState.Values
                    .SelectMany(v => v.Errors)
                    .Select(e => e.ErrorMessage)));

            var errors = context.ModelState
                .Where(x => x.Value?.Errors.Count > 0)
                .ToDictionary(
                    kvp => kvp.Key,
                    kvp => kvp.Value!.Errors.Select(e => e.ErrorMessage).ToArray()
                );

            var problemDetails = new ValidationProblemDetails(errors)
            {
                Status = StatusCodes.Status400BadRequest,
                Title = "One or more validation errors occurred",
                Instance = context.HttpContext.Request.Path
            };

            context.Result = new BadRequestObjectResult(problemDetails);
            return; // Не викликаємо next() - action не виконується
        }

        // Кастомна валідація: перевірка бізнес-правил
        foreach (var argument in context.ActionArguments.Values)
        {
            if (argument is CreateProductDto dto)
            {
                // Приклад бізнес-правила: ціна має бути кратною 0.01
                if (dto.Price % 0.01m != 0)
                {
                    context.Result = new BadRequestObjectResult(new ProblemDetails
                    {
                        Status = StatusCodes.Status400BadRequest,
                        Title = "Invalid Price Format",
                        Detail = "Price must have at most 2 decimal places",
                        Instance = context.HttpContext.Request.Path
                    });
                    return;
                }
            }
        }

        _logger.LogInformation("Validation passed for {Action}", context.ActionDescriptor.DisplayName);

        // Валідація пройшла - виконуємо action
        await next();
    }
}

Ключові моменти:

  1. IAsyncActionFilter — виконується безпосередньо до/після action
  2. ModelState.IsValid — перевірка DataAnnotations
  3. Кастомна валідація — бізнес-правила, що не можна виразити через атрибути
  4. context.Result — якщо встановлено, action не виконується
  5. await next() — виконує action та наступні фільтри

3. ResponseWrapperFilter — Envelope Pattern

Створіть файл Filters/ResponseWrapperFilter.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace EcommerceFiltersApi.Filters;

public class ResponseWrapperFilter : IAsyncResultFilter
{
    public async Task OnResultExecutionAsync(
        ResultExecutingContext context,
        ResultExecutionDelegate next)
    {
        // Обгортаємо тільки успішні відповіді (200-299)
        if (context.Result is ObjectResult objectResult && 
            objectResult.StatusCode >= 200 && 
            objectResult.StatusCode < 300)
        {
            var wrappedResponse = new ApiResponse<object>
            {
                Success = true,
                Data = objectResult.Value,
                Meta = new ResponseMeta
                {
                    Timestamp = DateTime.UtcNow,
                    Version = "1.0",
                    Path = context.HttpContext.Request.Path
                }
            };

            context.Result = new ObjectResult(wrappedResponse)
            {
                StatusCode = objectResult.StatusCode
            };
        }

        await next();
    }
}

// DTO для обгортки
public record ApiResponse<T>
{
    public bool Success { get; init; }
    public T? Data { get; init; }
    public ResponseMeta? Meta { get; init; }
}

public record ResponseMeta
{
    public DateTime Timestamp { get; init; }
    public string Version { get; init; } = "1.0";
    public string? Path { get; init; }
}

Результат:

{
  "success": true,
  "data": {
    "id": 1,
    "name": "Laptop",
    "price": 1499.99
  },
  "meta": {
    "timestamp": "2024-01-15T10:30:00Z",
    "version": "1.0",
    "path": "/api/products/1"
  }
}

4. CorrelationIdFilter — Traceability

Створіть файл Filters/CorrelationIdFilter.cs:

using Microsoft.AspNetCore.Mvc.Filters;
using System.Diagnostics;

namespace EcommerceFiltersApi.Filters;

public class CorrelationIdFilter : IAsyncActionFilter
{
    private const string CorrelationIdHeader = "X-Correlation-ID";

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        // Отримуємо або генеруємо Correlation ID
        var correlationId = context.HttpContext.Request.Headers[CorrelationIdHeader]
            .FirstOrDefault() ?? Guid.NewGuid().ToString();

        // Додаємо до response headers
        context.HttpContext.Response.Headers.Append(CorrelationIdHeader, correlationId);

        // Додаємо до Activity для distributed tracing
        Activity.Current?.SetTag("correlation.id", correlationId);

        // Додаємо до HttpContext для доступу в контролерах
        context.HttpContext.Items["CorrelationId"] = correlationId;

        await next();
    }
}

5. PerformanceMonitoringFilter — Метрики

Створіть файл Filters/PerformanceMonitoringFilter.cs:

using Microsoft.AspNetCore.Mvc.Filters;
using System.Diagnostics;

namespace EcommerceFiltersApi.Filters;

public class PerformanceMonitoringFilter : IAsyncActionFilter
{
    private readonly ILogger<PerformanceMonitoringFilter> _logger;
    private readonly int _warningThresholdMs;

    public PerformanceMonitoringFilter(
        ILogger<PerformanceMonitoringFilter> logger,
        IConfiguration configuration)
    {
        _logger = logger;
        _warningThresholdMs = configuration.GetValue<int>("Performance:WarningThresholdMs", 1000);
    }

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        var stopwatch = Stopwatch.StartNew();
        var actionName = context.ActionDescriptor.DisplayName;
        var method = context.HttpContext.Request.Method;
        var path = context.HttpContext.Request.Path;

        _logger.LogInformation(
            "Starting {Method} {Path} ({Action})",
            method,
            path,
            actionName);

        // Виконуємо action
        var resultContext = await next();

        stopwatch.Stop();
        var elapsedMs = stopwatch.ElapsedMilliseconds;

        // Додаємо час виконання до response headers
        context.HttpContext.Response.Headers.Append("X-Response-Time-Ms", elapsedMs.ToString());

        // Логуємо з різними рівнями залежно від часу виконання
        if (elapsedMs > _warningThresholdMs)
        {
            _logger.LogWarning(
                "SLOW REQUEST: {Method} {Path} completed in {ElapsedMs}ms (threshold: {Threshold}ms)",
                method,
                path,
                elapsedMs,
                _warningThresholdMs);
        }
        else
        {
            _logger.LogInformation(
                "{Method} {Path} completed in {ElapsedMs}ms",
                method,
                path,
                elapsedMs);
        }

        // Додаємо метрики до HttpContext для можливого використання в інших фільтрах
        context.HttpContext.Items["PerformanceMetrics"] = new
        {
            ElapsedMilliseconds = elapsedMs,
            ActionName = actionName,
            IsSlowRequest = elapsedMs > _warningThresholdMs
        };
    }
}

Результат у логах:

[INFO] Starting GET /api/products/1 (ProductsController.GetById)
[INFO] GET /api/products/1 completed in 45ms
[WARN] SLOW REQUEST: GET /api/products/search completed in 1250ms (threshold: 1000ms)

Крок 3: Реєстрація фільтрів

Є три способи реєстрації фільтрів:

Спосіб 1: Глобальна реєстрація (для всіх endpoints)

// Program.cs
builder.Services.AddControllers(options =>
{
    // Фільтри виконуються у порядку додавання
    options.Filters.Add<CorrelationIdFilter>();
    options.Filters.Add<PerformanceMonitoringFilter>();
    options.Filters.Add<RequestValidationFilter>();
    options.Filters.Add<ResponseWrapperFilter>();
});

// Реєструємо фільтри у DI для можливості ін'єкції залежностей
builder.Services.AddScoped<ApiKeyAuthFilter>();
builder.Services.AddScoped<RequestValidationFilter>();
builder.Services.AddScoped<ResponseWrapperFilter>();
builder.Services.AddScoped<CorrelationIdFilter>();
builder.Services.AddScoped<PerformanceMonitoringFilter>();

Спосіб 2: На рівні контролера

[ApiController]
[Route("api/[controller]")]
[ServiceFilter(typeof(ApiKeyAuthFilter))] // Для всіх методів контролера
public class ProductsController : ControllerBase
{
    // ...
}

Спосіб 3: На рівні методу

[HttpPost]
[ServiceFilter(typeof(RequestValidationFilter))] // Тільки для цього методу
public async Task<IActionResult> Create(CreateProductDto dto)
{
    // ...
}
ServiceFilter vs TypeFilter:
  • [ServiceFilter(typeof(MyFilter))] — фільтр береться з DI (потрібна реєстрація)
  • [TypeFilter(typeof(MyFilter))] — фільтр створюється щоразу (без DI)
Використовуйте ServiceFilter для фільтрів з залежностями (ILogger, IConfiguration).

Повна конфігурація Program.cs

using Microsoft.EntityFrameworkCore;
using EcommerceFiltersApi.Data;
using EcommerceFiltersApi.Filters;

var builder = WebApplication.CreateBuilder(args);

// Реєстрація DbContext
builder.Services.AddDbContext<ProductDbContext>(options =>
    options.UseInMemoryDatabase("ProductsDb"));

// Реєстрація фільтрів у DI
builder.Services.AddScoped<ApiKeyAuthFilter>();
builder.Services.AddScoped<RequestValidationFilter>();
builder.Services.AddScoped<ResponseWrapperFilter>();
builder.Services.AddScoped<CorrelationIdFilter>();
builder.Services.AddScoped<PerformanceMonitoringFilter>();

// Налаштування Controllers з глобальними фільтрами
builder.Services.AddControllers(options =>
{
    // Порядок важливий!
    options.Filters.Add<CorrelationIdFilter>();        // 1. Додаємо Correlation ID
    options.Filters.Add<PerformanceMonitoringFilter>(); // 2. Починаємо вимірювання
    options.Filters.Add<RequestValidationFilter>();     // 3. Валідуємо запит
    options.Filters.Add<ResponseWrapperFilter>();       // 4. Обгортаємо відповідь
});

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Ініціалізація бази даних
using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<ProductDbContext>();
    db.Database.EnsureCreated();
}

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

Конфігурація appsettings.json

{
  "ApiKeys": [
    "sk_test_abc123xyz",
    "sk_live_def456uvw"
  ],
  "Performance": {
    "WarningThresholdMs": 1000
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "EcommerceFiltersApi.Filters": "Information"
    }
  }
}

Крок 4: Створення контролера

Створіть файл Controllers/ProductsController.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using EcommerceFiltersApi.Data;
using EcommerceFiltersApi.Models;
using EcommerceFiltersApi.Filters;

namespace EcommerceFiltersApi.Controllers;

[ApiController]
[Route("api/[controller]")]
[ServiceFilter(typeof(ApiKeyAuthFilter))] // API key для всього контролера
public class ProductsController : ControllerBase
{
    private readonly ProductDbContext _db;
    private readonly ILogger<ProductsController> _logger;

    public ProductsController(ProductDbContext db, ILogger<ProductsController> logger)
    {
        _db = db;
        _logger = logger;
    }

    /// <summary>
    /// Отримати всі продукти
    /// </summary>
    [HttpGet]
    [ProducesResponseType(typeof(ApiResponse<IEnumerable<Product>>), StatusCodes.Status200OK)]
    public async Task<ActionResult<IEnumerable<Product>>> GetAll()
    {
        // Отримуємо Correlation ID з HttpContext (додано фільтром)
        var correlationId = HttpContext.Items["CorrelationId"]?.ToString();
        _logger.LogInformation("Fetching all products. CorrelationId: {CorrelationId}", correlationId);

        var products = await _db.Products
            .Where(p => p.IsActive)
            .ToListAsync();

        return Ok(products);
    }

    /// <summary>
    /// Отримати продукт за ID
    /// </summary>
    [HttpGet("{id:int}")]
    [ProducesResponseType(typeof(ApiResponse<Product>), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<Product>> GetById(int id)
    {
        var product = await _db.Products.FindAsync(id);

        if (product is null)
            return NotFound();

        return Ok(product);
    }

    /// <summary>
    /// Створити новий продукт
    /// </summary>
    [HttpPost]
    [ProducesResponseType(typeof(ApiResponse<Product>), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<Product>> Create(CreateProductDto dto)
    {
        // Валідація вже пройшла через RequestValidationFilter
        
        var product = new Product
        {
            Name = dto.Name,
            Price = dto.Price,
            Stock = dto.Stock
        };

        _db.Products.Add(product);
        await _db.SaveChangesAsync();

        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }

    /// <summary>
    /// Видалити продукт
    /// </summary>
    [HttpDelete("{id:int}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Delete(int id)
    {
        var product = await _db.Products.FindAsync(id);

        if (product is null)
            return NotFound();

        _db.Products.Remove(product);
        await _db.SaveChangesAsync();

        return NoContent();
    }
}

Ключові моменти:

  1. Чистий код — контролер не захаращений логікою валідації, логування, обгортання
  2. Доступ до даних фільтрів — через HttpContext.Items (CorrelationId, ApiKey)
  3. Автоматична обробка — всі фільтри виконуються автоматично
  4. Типізовані відповідіApiResponse<T> для Swagger документації

Крок 5: Тестування фільтрів

bash
$ dotnet run
info: Now listening on: https://localhost:5001
# Тест 1: Без API ключа (401)
$ curl https://localhost:5001/api/products
HTTP/1.1 401 Unauthorized
{ "title": "API Key Required" }
# Тест 2: З валідним API ключем (200)
$ curl -H "X-Api-Key: sk_test_abc123xyz" https://localhost:5001/api/products
HTTP/1.1 200 OK
X-Correlation-ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
X-Response-Time-Ms: 45
{
"success": true,
"data": [{ "id": 1, "name": "Laptop" }],
"meta": { "timestamp": "2024-01-15T10:30:00Z" }
}
# Тест 3: Валідаційна помилка (400)
$ curl -X POST https://localhost:5001/api/products \
-H "X-Api-Key: sk_test_abc123xyz" \
-H "Content-Type: application/json" \
-d '{"name":"","price":-10}'
HTTP/1.1 400 Bad Request
{
"errors": {
"name": ["Product name is required"],
"price": ["Price must be between 0.01 and 1,000,000"]
}
}

Просунуті техніки

Order — Контроль порядку виконання

Фільтри виконуються у порядку їх реєстрації, але можна явно вказати порядок:

public class HighPriorityFilter : IAsyncActionFilter, IOrderedFilter
{
    public int Order => -100; // Виконається раніше (менше число = вища пріоритет)

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        // ...
        await next();
    }
}

public class LowPriorityFilter : IAsyncActionFilter, IOrderedFilter
{
    public int Order => 100; // Виконається пізніше

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        // ...
        await next();
    }
}

Порядок виконання:

HighPriorityFilter (Order: -100)
  → NormalFilter (Order: 0, за замовчуванням)
    → LowPriorityFilter (Order: 100)
      → Action Method
    ← LowPriorityFilter
  ← NormalFilter
← HighPriorityFilter

Short-Circuiting — Припинення pipeline

Фільтр може зупинити виконання pipeline, встановивши context.Result:

public class CacheFilter : IAsyncActionFilter
{
    private readonly IMemoryCache _cache;

    public CacheFilter(IMemoryCache cache)
    {
        _cache = cache;
    }

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        var cacheKey = context.HttpContext.Request.Path.ToString();

        // Перевіряємо кеш
        if (_cache.TryGetValue(cacheKey, out object? cachedResult))
        {
            // Short-circuit: повертаємо з кешу, action не виконується
            context.Result = new OkObjectResult(cachedResult);
            return; // НЕ викликаємо next()
        }

        // Виконуємо action
        var resultContext = await next();

        // Кешуємо результат
        if (resultContext.Result is OkObjectResult okResult)
        {
            _cache.Set(cacheKey, okResult.Value, TimeSpan.FromMinutes(5));
        }
    }
}

Conditional Filters — Умовне застосування

Фільтр може вирішувати, чи застосовувати логіку:

public class ConditionalLoggingFilter : IAsyncActionFilter
{
    private readonly ILogger<ConditionalLoggingFilter> _logger;

    public ConditionalLoggingFilter(ILogger<ConditionalLoggingFilter> logger)
    {
        _logger = logger;
    }

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        // Логуємо тільки POST/PUT/DELETE
        var shouldLog = context.HttpContext.Request.Method != "GET";

        if (shouldLog)
        {
            _logger.LogInformation(
                "Modifying request: {Method} {Path}",
                context.HttpContext.Request.Method,
                context.HttpContext.Request.Path);
        }

        await next();
    }
}

Attribute-Based Configuration

Створення кастомного атрибута для конфігурації фільтра:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequireApiKeyAttribute : Attribute, IFilterFactory
{
    public bool IsReusable => false;

    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        return serviceProvider.GetRequiredService<ApiKeyAuthFilter>();
    }
}

// Використання
[RequireApiKey] // Замість [ServiceFilter(typeof(ApiKeyAuthFilter))]
public class ProductsController : ControllerBase
{
    // ...
}

Практичні завдання

Рівень 1: Базове розуміння

Завдання 1.1: Порядок виконання

У якому порядку виконаються наступні фільтри?

options.Filters.Add<FilterA>(); // Order: 0
options.Filters.Add<FilterB>(); // Order: -50
options.Filters.Add<FilterC>(); // Order: 100

Завдання 1.2: Вибір типу фільтра

Який тип фільтра використати для кожного сценарію?

  1. Валідація API-ключа
  2. Логування часу виконання
  3. Обгортання відповіді у envelope
  4. Перевірка прав доступу до ресурсу

Рівень 2: Логіка та розширення

Завдання 2.1: Rate Limiting Filter

Створіть фільтр, що обмежує кількість запитів до 10 на хвилину для кожного API-ключа:

::

::

Завдання 2.2: Request/Response Logging Filter

Створіть фільтр, що логує тіло запиту та відповіді для аудиту:

Завдання 2.3: Conditional Response Compression

Створіть фільтр, що додає заголовок Content-Encoding: gzip для великих відповідей:

::


Рівень 3: Архітектура та створення

Завдання 3.1: Multi-Tenant API Key Filter

Створіть систему фільтрів для multi-tenant API, де кожен API-ключ прив'язаний до tenant:

Завдання 3.2: Audit Trail Filter

Створіть систему аудиту, що записує всі зміни даних (POST/PUT/DELETE) у базу даних:

Завдання 3.3: Feature Flag Filter

Створіть фільтр для керування доступом до експериментальних features через feature flags:


Резюме

У цій статті ви навчилися використовувати фільтри для реалізації cross-cutting concerns у Web API:

Ключові концепції

1. Типи фільтрів для API:

  • Authorization Filters — API key validation, custom auth
  • Action Filters — валідація DTO, логування, correlation IDs
  • Result Filters — response wrapping (envelope pattern), headers
  • Exception Filters — перетворення винятків у ProblemDetails

2. Filter Pipeline:

  • Фільтри виконуються у чіткому порядку навколо action методу
  • IOrderedFilter дозволяє контролювати порядок виконання
  • Short-circuiting через context.Result зупиняє pipeline

3. Реєстрація фільтрів:

  • Глобальноoptions.Filters.Add<T>() для всіх endpoints
  • На контролері[ServiceFilter(typeof(T))] для всіх методів
  • На методі[ServiceFilter(typeof(T))] для конкретного endpoint

4. Практичні патерни:

  • Envelope Pattern — обгортання відповідей у стандартну структуру
  • Correlation IDs — traceability для distributed systems
  • Performance Monitoring — вимірювання часу виконання
  • Multi-Tenancy — ізоляція даних між клієнтами
  • Audit Trail — логування всіх змін даних
  • Feature Flags — керування доступом до експериментальних features

Переваги фільтрів

Централізація логіки — без дублювання коду
Чистий код контролерів — бізнес-логіка не захаращена технічними деталями
Тестованість — фільтри легко тестувати ізольовано
Консистентність — однакова поведінка для всіх endpoints
Масштабованість — легко додавати нові cross-cutting concerns

Коли використовувати фільтри

СценарійРішення
Валідація API-ключаAuthorization Filter
Логування запитів/відповідейAction Filter
Вимірювання продуктивностіAction Filter
Обгортання відповідейResult Filter
Додавання headersResult Filter
Rate limitingAction Filter
Multi-tenancyAuthorization + Action Filters
Audit trailAction Filter
Feature flagsAction Filter
Best Practice: Використовуйте фільтри для технічних аспектів (логування, валідація, авторизація), а бізнес-логіку тримайте у сервісах та контролерах.

Додаткові ресурси

Офіційна документація


Наступна стаття:Пагінація, фільтрація та сортування — практична реалізація PagedList<T>, query-based фільтрація, dynamic сортування та HATEOAS links.