새 C#API 생성: 전송 요청 확인

트위터에서 저를 팔로우해 주세요. 구독Newsletter | 최초로 timdeschryver.dev에 발표되었습니다.
성공한 장기 소프트웨어의 관건은 양호한 구조 설계이다.
좋은 디자인은 개발자로 하여금 새로운 기능을 쉽게 작성할 수 있을 뿐만 아니라, 변화에 적응하여 장애를 받지 않게 할 수 있다.
이것이 바로 그가 책Domain Driven Design에서 말한 부드러운 디자인이다.

To have a project accelerate as development proceeds—rather than get weighed down by its own legacy—demands a design that is a pleasure to work with, inviting to change. A supple design.


좋은 디자인은 응용 프로그램의 핵심 분야에 주목한다.
안타깝게도 이 분야는 이 층에 속하지 않는 책임으로 가득 차기 쉽다.
매번 추가되면서 핵심 분야를 읽고 이해하는 것은 더욱 어려워졌다.
마찬가지로, 하나가 늘어날 때마다 미래의 일은 더욱 어려워진다.
따라서 애플리케이션 로직에서 도메인 계층을 보호하는 것이 중요합니다.
주범 중 하나는 전입 요청에 대한 검증이다.
이러한 논리가 도메인 레벨에 침투하는 것을 방지하기 위해서, 우리는 요청이 도메인 레벨에 도달하기 전에 이를 검증하기를 희망합니다.
이 박문에서 우리는 어떻게 역층에서 검증을 추출하는지 배울 것이다.
시작하기 전에, 본고는 API가 command pattern 를 사용하여 전송된 요청을 명령이나 조회로 전환한다고 가정합니다.이 블로그의 모든 부분은 MediatR 패키지를 사용했고 또 다른 유행하는 선택은 Brighter이다.
command pattern의 장점은 핵심 역 논리와 API 층을 결합시키는 것이다(우리는 두꺼운 컨트롤러가 필요하지 않다).
대부분의 실현command pattern 라이브러리는 연결할 수 있는 중간부품 파이프도 공개했다.이것은 모든 스케줄링 명령에서 실행해야 할 응용 프로그램 논리를 추가할 수 있는 해결 방안과 집중적인 위치를 제공하기 때문에 매우 유용하다.

MediatR 요청
새로운 record 형식 introduced in C# 9 을 사용하면 정의 요청은 한 줄의 프로그램이 됩니다.
또 다른 장점은 실례가 변할 수 없고 부작용이 없기 때문에 명령을 예측할 수 있고 믿을 수 있다는 것이다.
record AddProductToCartCommand(Guid CartId, string Sku, int Amount) : MediatR.IRequest;
상기 명령을 조정하기 위해 전송된 요청이 컨트롤러에 비추어집니다.
[ApiController]
[Route("[controller]")]
public class CustomerCartsController : ControllerBase
{
    private readonly IMediator _mediator;

    public CustomerCartsController(IMediator mediator)
        => _mediator = mediator;

    [HttpPost("{cartId}")]
    public async Task<IActionResult> AddProductToCart(Guid cartId, [FromBody] CartProduct cartProduct)
    {
        await _mediator.Send(new AddProductToCartCommand(cartId, cartProduct.Sku, cartProduct.Amount));
        return Ok();
    }
}

MediatR 인증 프로그램 동작
우리는 이 명령을 컨트롤러나 처리하는 영역에서 AddProductToCartCommand 명령을 검증하는 것이 아니라 MediatR 파이프를 사용할 것입니다.
파이프 동작을 사용하면 처리 프로그램이 명령을 처리하기 전이나 이후에 논리를 실행할 수 있습니다.
이 예에서는 처리 프로그램 (필드) 에 도착하기 전에 명령이 검증될 수 있는 집중적인 위치를 제공합니다.
명령이 처리 프로그램에 도착했을 때, 우리는 명령이 유효한지 더 이상 걱정할 필요가 없다.
잘못된 명령을 어떻게 처리하는지에 대한 표준화된 해결 방안도 강제로 실행한다.
비록 이것은 보잘것없는 변화인 것 같지만, 역층의 모든 처리 프로그램을 분리할 것이다.
이상적인 상황에서, 우리는 단지 처리 영역에서 업무 논리를 처리하기를 희망할 뿐이다.
검증 논리를 삭제하면 사상을 해방시킬 수 있다. 그러면 우리는 업무 논리에만 관심을 가질 수 있고, 편성 코드를 읽거나 쓸 필요가 없다.
검증 논리는 집중적이기 때문에 모든 명령이 검증을 거쳐 균열에서 빠져나가는 명령이 없다는 것을 확보한다.
다음 코드 세션에서 명령을 검증하기 위해 새로운 파이프 동작 ValidatorPipelineBehavior 을 만들었습니다.
명령을 보낼 때 ValidatorPipelineBehavior 처리 프로그램은 명령 처리 프로그램에 도착하기 전에 명령을 받는다.ValidatorPipelineBehavior 이 종류의 인증서를 호출하여 명령이 유효한지 확인합니다.
요청이 유효할 때만 다음 처리 프로그램에 요청을 전달할 수 있습니다.
그렇지 않으면 InputValidationException이상을 던진다.
우리는 Validation with FluentValidation에서 어떻게 검증기를 만드는지 볼 것이다.
현재 중요한 것은 요청이 무효일 때 검증 메시지를 되돌려준다는 것을 알아야 한다. (인용이 무효한 속성)유효성 검사 세부 정보가 예외에 추가되고 나중에 응답 생성에 사용됩니다.
public class ValidatorPipelineBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidatorPipelineBehavior(IEnumerable<IValidator<TRequest>> validators)
      => _validators = validators;

    public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        // Invoke the validators
        var failures = _validators
            .Select(validator => validator.Validate(request))
            .SelectMany(result => result.Errors)
            .ToArray();

        if (failures.Length > 0)
        {
            // Map the validation failures and throw an error,
            // this stops the execution of the request
            var errors = failures
                .GroupBy(x => x.PropertyName)
                .ToDictionary(k => k.Key, v => v.Select(x => x.ErrorMessage).ToArray());
            throw new InputValidationException(errors);
        }

        // Invoke the next handler
        // (can be another pipeline behavior or the request handler)
        return next();
    }
}

FluentValidation을 사용하여 검증
요청을 검증하기 위해서 저는 FluentValidation 라이브러리를 즐겨 사용합니다.
FluentValidation에서 실현IRequest추상류를 통해 AbstractValidator기록에 따라'규칙'을 정의한다.
FluentValidation을 사용하는 이유는 다음과 같습니다.
  • 검증 규칙과 모델
  • 사이에 분리가 있음
  • 쓰기 및 읽기 용이
  • 내장된 많은 검증기를 제외하고는 사용자 정의 규칙
  • 을 만들 수 있습니다.
  • 확장성
  • public class AddProductToCartCommandValidator : FluentValidation.AbstractValidator<AddProductToCartCommandCommand>
    {
        public AddProductToCartCommandValidator()
        {
            RuleFor(x => x.CartId)
                .NotEmpty();
    
            RuleFor(x => x.Sku)
                .NotEmpty();
    
            RuleFor(x => x.Amount)
                .GreaterThan(0);
        }
    }
    

    MediatR 및 FluentValidation 등록
    현재 우리는 파이프 행위를 검증하기 위해 인증서를 만들었고 DI 용기에 등록할 수 있습니다.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        // Register all Mediatr Handlers
        services.AddMediatR(typeof(Startup));
    
        // Register custom pipeline behaviors
        services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidatorPipelineBehavior<,>));
    
        // Register all Fluent Validators
        services
            .AddMvc()
            .AddFluentValidation(s => s.RegisterValidatorsFromAssemblyContaining<Startup>());
    }
    

    HTTP API에 대한 문제 세부 정보
    이제 첫 번째 부탁을 할 준비가 다 됐습니다.
    잘못된 요청을 시도하고 보낼 때 내부 서버 오류 (500) 응답을 받을 것입니다.
    이것은 매우 좋지만 결코 소비자의 좋은 체험을 반영할 수 없다.나는 우리가 더 잘 할 수 있을 것이라고 믿는다.
    소비자에게 사용자(인터페이스), 동료 개발자(또는 당신 자신), 심지어 제3자에게 더 좋은 체험을 하기 위해 강화된 결과는 요청이 실패한 원인을 명확하게 할 것이다.이런 방법은 API와의 통합을 더욱 쉽고, 더욱 좋고, 더욱 빠르게 할 수 있다.
    나는 반드시 제3자 서비스와 통합해야 하지만 이러한 서비스들은 이 점을 기억하지 못한다.
    이것은 나에게 많은 좌절을 가져다 주었고, 융합이 마침내 끝났을 때, 나는 매우 기뻤다.
    나는 실패 요청에 대한 응답에 대해 더 많은 고려를 한다면 실현이 더욱 빠르고 최종 결과도 더욱 좋을 것이라고 믿는다.유감스럽게도 대부분의 서비스와 통합은 표준보다 낮은 체험을 제공했다.
    이 경험을 통해 저는 미래의 자신과 다른 개발자를 돕고 더 좋은 반응을 제공할 수 있도록 최선을 다했습니다.더 좋은 것은 표준화된 응답을 Problem Details for HTTP APIs라고도 부른다.
    이거.NET 프레임워크는 문제의 세부 규범을 실현하는 클래스ProblemDetails를 제공했다.
    사실 하나.NET API는 일부 잘못된 요청에 대한 세부 정보 응답을 반환합니다.
    예를 들어, 라우트에서 숫자 1이 아닌 단어one를 사용할 경우NET가 다음 응답을 반환합니다.
    {
      "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
      "title": "One or more validation errors occurred.",
      "status": 400,
      "traceId": "00-6aac4e84d1d4054f92ac1d4334c48902-25e69ea91f518045-00",
      "errors": {
        "id": ["The value 'one' is not valid."]
      }
    }
    

    응답(이상)을 문제 세부 정보에 매핑
    문제의 세부 사항을 실현하기 위해서, 우리는 exceptions middleware 또는 exception filter 응답을 덮어쓸 수 있습니다.
    다음 코드 세션에서, 우리는 응용 프로그램에서 이상을 일으킬 때, 이상에 대한 상세한 정보를 검색하기 위해 중간부품을 사용합니다.
    이러한 이상 세부 정보를 바탕으로 문제 세부 정보 대상을 구축한다.
    모든 버퍼링된 이상은 중간부품에 의해 포착되기 때문에 모든 이상에 대해 특정한 문제에 대한 상세한 정보를 만들 수 있습니다.
    아래의 예에서 InputValidationException이상만 비치고 나머지 이상은 모두 동등하게 취급된다.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseExceptionHandler(errorApp =>
        {
            errorApp.Run(async context =>
            {
                var errorFeature = context.Features.Get<IExceptionHandlerFeature>();
                var exception = errorFeature.Error;
    
                // https://tools.ietf.org/html/rfc7807#section-3.1
                var problemDetails = new ProblemDetails
                {
                    Type = $"https://example.com/problem-types/{exception.GetType().Name}",
                    Title = "An unexpected error occurred!",
                    Detail = "Something went wrong",
                    Instance = errorFeature switch
                    {
                        ExceptionHandlerFeature e => e.Path,
                        _ => "unknown"
                    },
                    Status = StatusCodes.Status400BadRequest,
                    Extensions =
                    {
                        ["trace"] = Activity.Current?.Id ?? context?.TraceIdentifier
                    }
                };
    
                switch (exception)
                {
                    case InputValidationException validationException:
                        problemDetails.Status = StatusCodes.Status403Forbidden;
                        problemDetails.Title = "One or more validation errors occurred";
                        problemDetails.Detail = "The request contains invalid parameters. More information can be found in the errors.";
                        problemDetails.Extensions["errors"] = validationException.Errors;
                        break;
                }
    
                context.Response.ContentType = "application/problem+json";
                context.Response.StatusCode = problemDetails.Status.Value;
                context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
                {
                    NoCache = true,
                };
                await JsonSerializer.SerializeAsync(context.Response.Body, problemDetails);
            });
        });
    
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
    비정상 처리 프로그램이 자리를 잡은 후 파이프 동작이 잘못된 명령을 감지하면 다음 응답을 되돌려줍니다.
    예를 들어 AddProductToCartCommand 명령MediatR Command을 음수로 보낼 때.
    {
      "type": "https://example.com/problem-types/InputValidationException",
      "title": "One or more validation errors occurred",
      "status": 403,
      "detail": "The request contains invalid parameters. More information can be found in the errors.",
      "instance": "/customercarts",
      "trace": "00-22fde64da9b70a4691e8c536aafb2c49-f90b88a19f1dca47-00",
      "errors": {
        "Amount": ["'Amount' must be greater than '0'."]
      }
    }
    
    사용자 정의 비정상 처리 프로그램을 만들고 문제의 상세한 정보에 비정상을 비추는 것 외에도 Hellang.Middleware.ProblemDetails 패키지를 사용할 수 있습니다.Hellang.Middleware.ProblemDetails 소프트웨어 패키지는 문제의 세부 사항에 이상을 쉽게 비추고 코드가 거의 필요하지 않다.

    일관된 문제 세부 사항
    마지막 질문이 하나 더 있습니다.
    위의 코드 세그먼트에서 예상되는 MediatR 요청은 컨트롤러에서 응용 프로그램이 만든 방법입니다.
    바디에 명령이 포함된 API 끝점은 .NET Model Validator에서 자동으로 검증됩니다.단점이 잘못된 명령을 받았을 때, 우리의 파이프와 이상 처리는 이 요청을 처리하지 않습니다.이것은 기본값을 의미합니다.문제 세부 정보 대신 NET 응답이 반환됩니다.
    예를 들어 AddProductToCart 단점은 AddProductToCartCommand 명령을 직접 수신하고 이 명령을 MediatR 파이프에 보낸다.
    [ApiController]
    [Route("[controller]")]
    public class CustomerCartsController : ControllerBase
    {
        private readonly IMediator _mediator;
    
        public CustomerCartsController(IMediator mediator)
            => _mediator = mediator;
    
        [HttpPost]
        public async Task<IActionResult> AddProductToCart(AddProductToCartCommand command)
        {
            await _mediator.Send(command);
            return Ok();
        }
    }
    
    처음에 나는 이런 상황이 발생할 줄 몰랐다. 나는 왜 이런 상황이 발생했는지, 그리고 응답 대상이 일치하도록 확보하는 방법을 깨닫는 데 시간이 걸렸다.가능한 복구 방법으로서, 우리는 이러한 기본 행위를 억제할 수 있으며, 이러한 무효한 요청은 우리의 파이프에서 처리될 것이다.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        // Register all Mediatr Handlers
        services.AddMediatR(typeof(Startup));
    
        // Register custom pipeline behaviors
        services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidatorPipelineBehavior<,>));
    
        // Register all Fluent Validators
        services
            .AddMvc()
            .AddFluentValidation(s => s.RegisterValidatorsFromAssemblyContaining<Startup>());
    
        services.Configure<ApiBehaviorOptions>(options => {
            options.SuppressModelStateInvalidFilter = true;
        });
    }
    
    하지만 단점이 하나 있다.잘못된 모델 필터를 억제함으로써 잘못된 기원 형식을 포착하지 않습니다.
    숫자가 필요하지만 문자열을 받는 단점에 대해 예상된 기원 형식 (이 예에서 숫자) 을 기본값 (이 예에서 0) 에 지정합니다.따라서 잘못된 모델 필터를 닫으면 의외의 오류가 발생할 수 있습니다.이전에는 이 작업으로 인해 잘못된 요청이 발생했습니다(400).
    이것이 바로 내가 단점에서 잘못된 입력을 받을 때 InputValidationException 이상을 던지는 것을 더 좋아하는 이유이다.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        // Register all Mediatr Handlers
        services.AddMediatR(typeof(Startup));
    
        // Register custom pipeline behaviors
        services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidatorPipelineBehavior<,>));
    
        // Register all Fluent Validators
        services
            .AddMvc()
            .AddFluentValidation(s => s.RegisterValidatorsFromAssemblyContaining<Startup>());
    
        services.Configure<ApiBehaviorOptions>(options => {
            options.InvalidModelStateResponseFactory = context => {
                var problemDetails = new ValidationProblemDetails(context.ModelState);
                throw new InputValidationException(problemDetails.Errors);
            };
        });
    }
    

    결론
    본고에서 우리는 MediatR 파이프를 사용하여 명령이 역층에 도착하기 전에 논리를 집중적으로 검증하는 방법을 보았다.이렇게 하면 모든 명령이 검증을 거쳐 처리 프로그램 (필드) 에 도착할 때 유효하다는 장점이 있다.다시 말하면 이 분야는 깨끗하고 간단해질 것이다.
    뚜렷한 분리가 존재하기 때문에 개발자는 뻔한 임무에만 전념할 수 있다.
    개발 과정에서 단원 테스트가 더욱 집중되고 작성하기 쉽다는 것을 알 수 있습니다.
    앞으로 필요하면 검증층을 교체하는 것도 쉬워질 것이다.
    표준화된 응답으로 Problem Details 오류를 지정하는 것도 알고 있습니다.
    규범에 따라 우리는 바퀴를 재발명할 필요가 없으며 API 소비자들에게 더욱 좋은 체험을 만들어 주었다.
    트위터에서 저를 팔로우해 주세요. 구독Newsletter | 최초로 timdeschryver.dev에 발표되었습니다.

    좋은 웹페이지 즐겨찾기