ASP.NET 응용 프로그램을 개발할 때 CQRS / MediatR이 그만한 가치가 있습니까?


17

최근 CQRS / MediatR을 살펴 보았습니다. 그러나 드릴 다운할수록 마음에 들지 않습니다. 아마도 나는 무언가 / 모든 것을 오해했을 것입니다.

따라서 컨트롤러를 이것으로 줄인다는 주장으로 시작됩니다.

public async Task<ActionResult> Edit(Edit.Query query)
{
    var model = await _mediator.SendAsync(query);

    return View(model);
}

얇은 컨트롤러 지침에 완벽하게 맞습니다. 그러나 오류 처리와 같은 매우 중요한 세부 사항은 생략합니다.

Login새로운 MVC 프로젝트에서 기본 동작을 볼 수 있습니다

public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
    ViewData["ReturnUrl"] = returnUrl;
    if (ModelState.IsValid)
    {
        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, set lockoutOnFailure: true
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
        {
            _logger.LogInformation(1, "User logged in.");
            return RedirectToLocal(returnUrl);
        }
        if (result.RequiresTwoFactor)
        {
            return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
        }
        if (result.IsLockedOut)
        {
            _logger.LogWarning(2, "User account locked out.");
            return View("Lockout");
        }
        else
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return View(model);
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

변환하면 실제 문제가 많이 발생합니다. 목표는 그것을 줄이는 것입니다

public async Task<IActionResult> Login(Login.Command command, string returnUrl = null)
{
    var model = await _mediator.SendAsync(command);

    return View(model);
}

이에 대한 한 가지 가능한 해결책 CommandResult<T>은 a 대신 a 를 반환 한 후 사후 조치 필터 model를 처리하는 것 CommandResult입니다. 여기서 논의한 바와 같이 .

구현 중 하나 CommandResult는 다음과 같습니다

public interface ICommandResult  
{
    bool IsSuccess { get; }
    bool IsFailure { get; }
    object Result { get; set; }
}

출처

그러나 Login여러 실패 상태가 있기 때문에 조치 에서 문제를 실제로 해결하지는 못합니다 . 이러한 추가 실패 상태를 추가 할 수 ICommandResult있지만 이는 매우 부풀린 클래스 / 인터페이스의 시작입니다. SRP (Single Responsibility)를 준수하지 않는다고 말할 수도 있습니다.

또 다른 문제는 returnUrl입니다. 이 return RedirectToLocal(returnUrl);코드 조각이 있습니다. 어떻게 든 명령의 성공 상태에 따라 조건부 인수를 처리해야합니다. 나는 그것을 할 수 있다고 생각하지만 (ModelBinder가 FromBody와 FromQuery ( returnUrlFromQuery) 인수를 단일 모델에 매핑 할 수 있는지 확실하지 않습니다 ). 어떤 종류의 미친 시나리오가 길을 떠날 수 있는지 궁금해 할 수 있습니다.

오류 메시지 반환과 함께 모델 검증도 더욱 복잡해졌습니다. 이것을 예로 들어

else
{
    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
    return View(model);
}

모델과 함께 오류 메시지를 첨부합니다. 이런 종류의 일은 모델이 필요하기 때문에 Exception전략을 사용하여 수행 할 수 없습니다 ( here 제안 ). 아마도 모델을 얻을 수는 Request있지만 매우 복잡한 프로세스 일 것입니다.

그래서이 "간단한"액션을 변환하는 데 어려움을 겪고 있습니다.

입력을 찾고 있습니다. 나는 여기에 완전히 잘못되어 있습니까?


6
이미 관련 문제를 잘 이해하고있는 것 같습니다. 장난감의 예는 유용성을 입증하지만 실제 실제 응용 프로그램의 실제에 의해 압착 될 때 필연적으로 넘어지는 장난감 예가있는 "실버 글 머리 기호"가 많이 있습니다.
Robert Harvey

MediatR 동작을 확인하십시오. 기본적으로 교차 절단 문제를 해결할 수있는 파이프 라인입니다.
fml

답변:


14

나는 당신이 사용하는 패턴을 너무 많이 기대하고 있다고 생각합니다. CQRS는 데이터베이스 에 대한 쿼리와 명령의 모델 차이를 해결하도록 특별히 설계되었으며 MediatR은 프로세스 중 메시징 라이브러리입니다. CQRS는 예상 한대로 비즈니스 로직이 필요하지 않다고 주장하지 않습니다. CQRS는 데이터 액세스 패턴이지만 리디렉션, 뷰, 컨트롤러 등의 프리젠 테이션 계층에 문제가 있습니다.

CQRS 패턴을 인증에 잘못 적용하고 있다고 생각합니다. 로그인을 사용하면 CQRS에서 명령으로 모델링 할 수 없습니다.

명령 : 시스템 상태를 변경하지만 값을 반환하지 않음 -Martin
Fowler CommandQuerySeparation

제 생각에는 인증은 CQRS에 대한 잘못된 도메인입니다. 인증을 사용하면 강력하고 일관된 동기식 요청-응답 흐름이 필요하므로 1. 사용자의 자격 증명을 확인합니다. 2. 사용자를위한 세션을 만듭니다. 3. 식별 한 다양한 에지 사례를 처리합니다. 4. 사용자를 즉시 ​​부여하거나 거부합니다. 답으로.

ASP.NET 응용 프로그램을 개발할 때 CQRS / MediatR이 그만한 가치가 있습니까?

CQRS는 매우 구체적인 용도를 가진 패턴입니다. CRUD에서 사용되는 레코드 모델을 사용하는 대신 쿼리 및 명령을 모델링하는 것이 목적입니다. 시스템이 복잡 해짐에 따라 뷰 요구는 종종 단일 레코드 또는 소수의 레코드를 표시하는 것보다 더 복잡하므로 쿼리는 응용 프로그램의 요구를 더 잘 모델링 할 수 있습니다. 마찬가지로 명령은 단일 레코드를 변경하는 CRUD 대신 많은 레코드에 대한 변경 사항을 나타낼 수 있습니다. 마틴 파울러 경고

다른 패턴과 마찬가지로 CQRS는 일부 지역에서는 유용하지만 다른 지역에서는 유용하지 않습니다. 많은 시스템이 CRUD 정신 모델에 적합하므로 해당 스타일로 수행해야합니다. CQRS는 모든 관련자들에게 정신적으로 큰 도약이므로 혜택이 가치가없는 한 해결해서는 안됩니다. CQRS를 성공적으로 사용했지만 지금까지 내가 경험 한 대부분의 사례는 그다지 좋지 않았으며 CQRS는 소프트웨어 시스템을 심각한 어려움에 빠뜨리는 데 큰 힘이되었습니다.
-마틴 파울러 CQRS

따라서 귀하의 질문에 답변하기 위해 CRQ가 적합한 경우 응용 프로그램을 설계 할 때 CQRS가 첫 번째 수단이되어서는 안됩니다. 귀하의 질문에 CQRS를 사용할 이유가 있음을 알려주지 않았습니다.

MediatR은 프로세스 내 메시징 라이브러리이며 요청 처리와 요청을 분리하는 것을 목표로합니다. 이 라이브러리를 사용하도록 디자인을 개선 할 것인지 다시 결정해야합니다. 저는 개인적으로 진행중인 메시지를 옹호하지 않습니다. 느슨한 연결은 메시징보다 간단한 방법으로 달성 할 수 있으므로 여기서 시작하는 것이 좋습니다.


1
나는 100 % 동의합니다. CQRS는 약간 과장된 것이므로 "그들"이 내가하지 않은 것을 보았다고 생각했습니다. CRUD 웹 앱에서 CQRS의 이점을 보는 데 어려움을 겪고 있습니다. 지금까지 유일한 시나리오는 CQRS + ES입니다.
Snæbjørn

내 새 직장에있는 어떤 사람이 MediatR을 아키텍처라고 주장하는 새로운 ASP.Net 시스템에 배치하기로 결정했습니다. 그가 만든 구현은 DDD, SOLID, DRY, KISS가 아닙니다. YAGNI로 가득 찬 작은 시스템입니다. 그리고 그것은 당신과 같은 몇 가지 의견 이후에 시작되었습니다. 아키텍처를 점진적으로 적용하기 위해 코드를 재구성하는 방법을 알아 내려고 노력 중입니다. 나는 비즈니스 계층 외부에서 CQRS에 대해 같은 의견을 가지고 있었고 그런 식으로 생각하는 몇 명의 숙련 된 개발자가 기쁘다.
MFedatto

CQRS / MediatR 통합 아이디어가 많은 YAGNI 및 KISS 부족과 관련이있을 수 있음을 확인하는 것은 약간 아이러니합니다. 실제로 리포지토리 패턴과 같은 인기있는 대안 중 일부는 리포지토리 클래스를 팽창시키고 강제로 YAGNI를 홍보합니다 이러한 인터페이스를 구현하려는 모든 루트 집계에 대해 많은 CRUD 작업을 지정하는 인터페이스를 사용하여 이러한 메서드를 사용하지 않거나 "구현되지 않은"예외로 채 웁니다. CQRS는 이러한 일반화를 사용하지 않기 때문에 필요한 것만 구현할 수 있습니다.
Lesair Valmont '

@LesairValmont Repository는 CRUD이어야합니다. "많은 CRUD 조작 지정"은 4 (또는 "list"인 5) 여야합니다. 보다 구체적인 쿼리 액세스 패턴이 있으면 리포지토리 인터페이스에 없어야합니다. 나는 사용되지 않은 저장소 방법의 문제에 결코 빠지지 않았다. 예를 들어 줄 수 있습니까?
사무엘

@Samuel : CQRS와 마찬가지로 특정 시나리오에서는 리포지토리 패턴이 완벽하다고 생각합니다. 실제로 대규모 응용 프로그램에는 리포지토리 패턴에 가장 적합한 부분과 CQRS의 혜택을받는 부분이 있습니다. 응용 프로그램의 해당 부분 (예 : 작업 기반 (CQRS) 대 CRUD (레포))에 따른 철학, 사용중인 ORM (있는 경우), 도메인 모델링 (예 : 예를 들어 DDD). 간단한 CRUD 카탈로그의 경우 CQRS는 과도하게 과도하게 사용되며 채팅과 같은 일부 실시간 협업 기능은 사용하지 않습니다.
Lesair Valmont

10

CQRS는 데이터 관리에 그치지 않고 응용 프로그램 계층 (또는 DDD 시스템에서 가장 자주 사용되는 경우 도메인)에 너무 많이 번지지 않는 경향이 있습니다. 반면에 MVC 응용 프로그램은 프레젠테이션 계층 응용 프로그램이므로 CQRS의 쿼리 / 지속성 코어와 상당히 잘 분리되어 있어야합니다.

주목할만한 또 다른 점 (기본 Login방법을 비교 하고 씬 컨트롤러를 원함) : 모범 ASP에 대한 기본 ASP.NET 템플릿 / 보일러 플레이트 코드를 정확히 따르지 않을 것입니다.

얇은 컨트롤러도 매우 읽기 쉽습니다. 내가 가진 각 컨트롤러에는 일반적으로 컨트롤러에 필요한 논리를 처리하는 "서비스"개체가 있습니다.

public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) {

    var result = _service.Login(model);
    switch (result) {
        case result.lockout: return View("Lockout");
        case result.ok: return RedirectToLocal(returnUrl);
        default: return View("GeneralError");
    }
}

여전히 얇지 만 코드의 작동 방식을 실제로 변경하지는 않았으며 처리를 서비스 메소드에 위임하면 컨트롤러 조치를 쉽게 소화시키는 것 외에 다른 목적을 제공하지 않습니다.

이 서비스 클래스는 여전히 필요에 따라 모델 / 애플리케이션에 로직을 위임 할 책임이 있습니다. 실제로 코드를 깔끔하게 유지하기 위해 컨트롤러를 약간만 확장하면됩니다. 서비스 방법도 일반적으로 매우 짧습니다.

중재자가 개념적으로 다른 것을 수행 할 것이라고 확신하지 않습니다. 기본 컨트롤러 논리를 컨트롤러에서 다른 곳으로 옮겨 처리 할 수 ​​있습니다.

(이 MediatR에 대해 들어 본 적이 없으며 github 페이지를 훑어 보아도 CQRS와는 달리 획기적인 것으로 보이지는 않습니다. 실제로, 당신은 또 다른 추상화 계층처럼 보입니다. 코드를 더 단순하게 보이게하여 코드를 복잡하게 만들 수 있지만, 그것은 단지 나의 첫 취임)


5

http 요청 https://www.youtube.com/watch?v=SUiWfhAhgQw 에 대한 그의 접근 방식에서 Jimmy Bogard의 NDC 프레젠테이션을 보는 것이 좋습니다.

그러면 Mediatr의 용도를 명확하게 알 수 있습니다.

지미는 패턴과 추상화를 눈에 띄지 않습니다. 그는 매우 실용적입니다. Mediatr은 제어기 조치를 정리합니다. 예외 처리에 대해서는 Execute와 같은 상위 클래스로 푸시합니다. 따라서 매우 깨끗한 컨트롤러 작업으로 끝납니다.

다음과 같은 것 :

public bool Execute<T>(Func<T> messageFunction)
{
    try
    {
        messageFunction();

        return true;
    }
    catch (ValidationException exception)
    {
        Errors = string.Join(Environment.NewLine, exception.Errors.Select(e => e.ErrorMessage));
        Logger.LogException(exception, "ValidationException caught in SiteController");
    }
    catch (SiteException exception)
    {
        Errors = exception.Message;
        Logger.LogException(exception);
    }
    catch (DbEntityValidationException dbEntityValidationException)
    {
        // Retrieve the error messages as a list of strings.
        var errorMessages = dbEntityValidationException.EntityValidationErrors
                .SelectMany(x => x.ValidationErrors)
                .Select(x => x.ErrorMessage);

        // Join the list to a single string.
        var fullErrorMessage = string.Join("; ", errorMessages);

        // Combine the original exception message with the new one.
        var exceptionMessage = string.Concat(dbEntityValidationException.Message, " The validation errors are: ", fullErrorMessage);

        Logger.LogError(exceptionMessage);

        // Throw a new DbEntityValidationException with the improved exception message.
        throw new DbEntityValidationException(exceptionMessage, dbEntityValidationException.EntityValidationErrors);                
    }
    catch (Exception exception)
    {
        Errors = "An error has occurred.";
        Logger.LogException(exception, "Exception caught in SiteController.");
    }

    // used to indicate that any transaction which may be in progress needs to be rolled back for this request.
    HttpContext.Items[UiConstants.Error] = true;

    Response.StatusCode = (int)HttpStatusCode.InternalServerError; // fail

    return false;
}

사용법은 다음과 같습니다.

[Route("api/licence")]
public IHttpActionResult Post(LicenceEditModel licenceEditModel)
{
    var updateLicenceCommand = new UpdateLicenceCommand { LicenceEditModel = licenceEditModel };
    int licenceId = -1;

    if (Execute(() => _mediator.Send(updateLicenceCommand)))
    {
        return JsonSuccess(licenceEditModel);
    }

    return JsonError(Errors);
}

희망이 도움이됩니다.


4

많은 사람들이 (나도 그렇게했다) 패턴을 라이브러리와 혼동한다. CQRS는 패턴 이지만 MediatR은 해당 패턴을 구현하는 데 사용할 수 있는 라이브러리 입니다.

MediatR 또는 프로세스 내 메시징 라이브러리없이 CQRS를 사용할 수 있으며 CQRS없이 MediatR을 사용할 수 있습니다.

public interface IProductsWriteService
{
    void CreateProduct(CreateProductCommand createProductCommand);
}

public interface IProductsReadService
{
    ProductDto QueryProduct(Guid guid);
}

CQS는 다음과 같습니다.

public interface IProductsService
{
    void CreateProduct(CreateProductCommand createProductCommand);
    ProductDto QueryProduct(Guid guid);
}

실제로, 입력 모델의 이름을 위와 같이 "명령"으로 지정할 필요는 없습니다 CreateProductCommand. 그리고 검색어 "Queries"를 입력하십시오. 명령과 쿼리는 모델이 아닌 메서드입니다.

CQRS는 책임 분리에 관한 것입니다 (읽기 방법은 쓰기 방법과 분리 된 별도의 장소에 있어야합니다). CQS의 확장이지만 CQS의 차이점은 이러한 메소드를 1 클래스에 넣을 수 있다는 것입니다. (책임 분리가 아니라 명령 쿼리 분리). 분리 대 분리 참조

에서 https://martinfowler.com/bliki/CQRS.html :

핵심은 정보를 읽는 데 사용하는 모델과 다른 모델을 사용하여 정보를 업데이트 할 수 있다는 개념입니다.

입력과 출력을위한 별도의 모델이 아니라 책임의 분리에 대해 혼동이 있습니다.

CQRS 및 ID 생성 제한

CQRS 또는 CQS를 사용할 때 직면하게 될 한 가지 한계가 있습니다

기술적으로 원래 설명 명령에서 새로 생성 된 객체에서 ID를 생성하는 쉬운 방법이 없기 때문에 어리석은 값 (void)을 반환해서는 안됩니다 : https : //.com/questions/4361889/how-to- 적용 할 때 get-id-in-create-cqrs .

따라서 데이터베이스를 사용하지 않고 매번 ID를 생성해야합니다.


자세한 내용을 보려면 https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf를 참조하십시오.


1
데이터베이스에서 새 데이터를 유지하기위한 CQRS 명령이 새로 생성 된 데이터베이스 ID를 반환 할 수 없다는 "확인"에 동의합니다. 오히려 이것이 철학적 인 문제라고 생각합니다. DDD 및 CQRS의 많은 부분이 데이터 불변성에 관한 것임을 기억하십시오. 두 번 생각하면 데이터를 유지하는 단순한 행동이 데이터 변형 작업이라는 것을 깨닫기 시작합니다. 또한 새로운 ID뿐만 아니라 기본 데이터, 트리거 및 데이터를 변경할 수있는 저장된 proc로 채워진 필드 일 수도 있습니다.
Lesair Valmont '

물론 새 항목을 인수로 사용하여 "ItemCreated"와 같은 이벤트를 보낼 수 있습니다. 요청-응답 프로토콜 만 처리하고 "true"CQRS를 사용하는 경우 id를 미리 알고 있어야 별도의 쿼리 함수에 전달할 수 있습니다. 많은 경우에 CQRS는 너무 과잉입니다. 당신은 그것없이 살 수 있습니다. 그것은 코드를 구성하는 방법 일뿐이며 주로 사용하는 프로토콜에 달려 있습니다.
Konrad

CQRS 없이도 데이터 불변성을 달성 할 수 있습니다
Konrad
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.