CQRS / MediatR có đáng không khi phát triển ứng dụng ASP.NET?


16

Gần đây tôi đã xem xét CQRS / MediatR. Nhưng tôi càng đi sâu vào thì tôi càng ít thích nó. Có lẽ tôi đã hiểu nhầm một cái gì đó / mọi thứ.

Vì vậy, nó bắt đầu tuyệt vời bằng cách tuyên bố giảm bộ điều khiển của bạn để điều này

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

    return View(model);
}

Mà phù hợp hoàn hảo với hướng dẫn bộ điều khiển mỏng. Tuy nhiên, nó để lại một số chi tiết khá quan trọng - xử lý lỗi.

Hãy xem Loginhành động mặc định từ một dự án MVC mới

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);
}

Chuyển đổi điều đó cho chúng ta một loạt các vấn đề trong thế giới thực. Hãy nhớ mục tiêu là giảm nó xuống

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

    return View(model);
}

Một giải pháp khả thi cho vấn đề này là trả về một CommandResult<T>thay vì a modelvà sau đó xử lý CommandResulttrong bộ lọc hành động bài. Như đã thảo luận ở đây .

Một thực hiện CommandResultcó thể là như thế này

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

nguồn

Tuy nhiên, điều đó không thực sự giải quyết vấn đề của chúng ta trong Loginhành động, bởi vì có nhiều trạng thái thất bại. Chúng ta có thể thêm các trạng thái lỗi bổ sung này vào ICommandResultnhưng đó là một khởi đầu tuyệt vời cho một lớp / giao diện rất cồng kềnh. Người ta có thể nói rằng nó không tuân thủ Trách nhiệm đơn lẻ (SRP).

Một vấn đề khác là returnUrl. Chúng tôi có return RedirectToLocal(returnUrl);đoạn mã này. Bằng cách nào đó chúng ta cần xử lý các đối số có điều kiện dựa trên trạng thái thành công của lệnh. Trong khi tôi nghĩ điều đó có thể được thực hiện (Tôi không chắc liệu ModelBinder có thể ánh xạ các đối số FromBody và FromQuery ( returnUrllà FromQuery) thành một mô hình không). Người ta chỉ có thể tự hỏi những loại kịch bản điên rồ nào có thể đi xuống đường.

Xác thực mô hình cũng trở nên phức tạp hơn cùng với việc trả về các thông báo lỗi. Lấy điều này làm ví dụ

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

Chúng tôi đính kèm một thông báo lỗi cùng với mô hình. Loại điều này không thể được thực hiện bằng cách sử dụng một Exceptionchiến lược (như được đề xuất ở đây ) bởi vì chúng ta cần mô hình. Có lẽ bạn có thể lấy mô hình từ Requestnhưng nó sẽ là một quá trình rất liên quan.

Vì vậy, tất cả trong tất cả tôi đang có một thời gian khó khăn để chuyển đổi hành động "đơn giản" này.

Tôi đang tìm kiếm đầu vào. Tôi hoàn toàn sai ở đây?


6
Âm thanh như bạn đã hiểu những mối quan tâm có liên quan khá tốt. Có rất nhiều "viên đạn bạc" có ví dụ về đồ chơi chứng minh tính hữu dụng của chúng, nhưng chắc chắn sẽ rơi khi chúng bị ép bởi thực tế của một ứng dụng thực tế, thực tế.
Robert Harvey

Kiểm tra các hành vi MediatR. Về cơ bản, đây là một đường ống cho phép bạn giải quyết các mối quan tâm xuyên suốt.
fml

Câu trả lời:


14

Tôi nghĩ rằng bạn đang mong đợi quá nhiều mẫu bạn đang sử dụng. CQRS được thiết kế đặc biệt để giải quyết sự khác biệt về mô hình giữa truy vấn và các lệnh tới cơ sở dữ liệu và MediatR chỉ là thư viện nhắn tin đang xử lý. CQRS không yêu cầu loại bỏ nhu cầu logic kinh doanh như bạn mong đợi. CQRS là một mẫu để truy cập dữ liệu, nhưng vấn đề của bạn là ở lớp trình bày - hướng dẫn, khung nhìn, bộ điều khiển.

Tôi nghĩ rằng bạn có thể đang áp dụng sai mẫu CQRS để xác thực. Với đăng nhập, nó không thể được mô hình hóa như một lệnh trong CQRS vì

Các lệnh: Thay đổi trạng thái của một hệ thống nhưng không trả về giá trị
- Martin Fowler CommandQuerySpayation

Theo tôi, xác thực là một miền nghèo cho CQRS. Với xác thực, bạn cần luồng phản hồi yêu cầu đồng bộ, nhất quán để bạn có thể 1. kiểm tra thông tin đăng nhập của người dùng 2. tạo phiên cho người dùng 3. xử lý bất kỳ trường hợp cạnh nào mà bạn đã xác định 4. cấp hoặc từ chối ngay lập tức đáp lại

CQRS / MediatR có đáng không khi phát triển ứng dụng ASP.NET?

CQRS là một mẫu có công dụng rất cụ thể. Mục đích của nó là mô hình hóa các truy vấn và lệnh thay vì có một mô hình cho các bản ghi như được sử dụng trong CRUD. Khi các hệ thống trở nên phức tạp hơn, nhu cầu của các khung nhìn thường phức tạp hơn là chỉ hiển thị một bản ghi hoặc một số bản ghi và một truy vấn có thể mô hình hóa tốt hơn các nhu cầu của ứng dụng. Các lệnh tương tự có thể biểu thị các thay đổi đối với nhiều bản ghi thay vì CRUD mà bạn thay đổi các bản ghi đơn lẻ. Martin Fowler cảnh báo

Giống như bất kỳ mô hình nào, CQRS hữu ích ở một số nơi, nhưng không phải ở những nơi khác. Nhiều hệ thống phù hợp với mô hình tinh thần CRUD, và do đó nên được thực hiện theo phong cách đó. CQRS là một bước nhảy vọt về mặt tinh thần đáng kể cho tất cả những người có liên quan, vì vậy không nên giải quyết trừ khi lợi ích đáng để nhảy. Mặc dù tôi đã sử dụng CQRS thành công, cho đến nay, phần lớn các trường hợp tôi gặp phải đều không tốt lắm, với CQRS được coi là một lực lượng đáng kể để đưa hệ thống phần mềm vào những khó khăn nghiêm trọng.
- Martin Fowler CQRS

Vì vậy, để trả lời câu hỏi của bạn, CQRS không nên là phương sách đầu tiên khi thiết kế một ứng dụng khi CRUD phù hợp. Không có gì trong câu hỏi của bạn cho tôi biết rằng bạn có lý do để sử dụng CQRS.

Đối với MediatR, đây là một thư viện nhắn tin đang xử lý, nó nhằm mục đích tách các yêu cầu khỏi việc xử lý yêu cầu. Bạn phải một lần nữa quyết định nếu nó sẽ cải thiện thiết kế của bạn để sử dụng thư viện này. Cá nhân tôi không phải là người ủng hộ việc nhắn tin trong quá trình. Khớp nối lỏng lẻo có thể đạt được theo những cách đơn giản hơn so với nhắn tin, và tôi khuyên bạn nên bắt đầu từ đó.


1
Tôi đồng ý 100%. CQRS chỉ là một chút cường điệu, vì vậy tôi nghĩ rằng "họ" đã thấy một cái gì đó tôi đã không làm. Bởi vì tôi đang gặp khó khăn khi thấy những lợi ích của CQRS trong các ứng dụng web CRUD. Cho đến nay kịch bản duy nhất là CQRS + ES có ý nghĩa với tôi.
Snæbjørn

Một số người trong công việc mới của tôi đã quyết định đưa MediatR lên hệ thống ASP.Net mới, tuyên bố nó là một kiến ​​trúc. Việc thực hiện mà anh ấy thực hiện không phải là DDD, cũng không phải RẮN, cũng không phải DRY, cũng không phải KISS. Nó là một hệ thống nhỏ chứa đầy YAGNI. Và nó đã bắt đầu lâu sau khi một số ý kiến ​​như của bạn, bao gồm của bạn. Tôi đang cố gắng tìm cách làm thế nào tôi có thể tái cấu trúc mã để điều chỉnh kiến ​​trúc của nó dần dần. Tôi có cùng quan điểm về CQRS bên ngoài một lớp kinh doanh và tôi rất vui vì có một số nhà phát triển đã thử nghiệm nghĩ như vậy.
MFedatto

Thật là hơi mỉa mai khi khẳng định rằng ý tưởng kết hợp CQRS / MediatR có thể liên quan đến rất nhiều YAGNI và thiếu KISS, khi thực sự một số lựa chọn thay thế phổ biến, như mẫu Kho lưu trữ, thúc đẩy YAGNI bằng cách làm mờ lớp kho lưu trữ và buộc các giao diện để chỉ định nhiều hoạt động CRUD trên tất cả các tập hợp gốc muốn thực hiện các giao diện đó, thường để các phương thức đó không được sử dụng hoặc chứa các ngoại lệ "không được triển khai". Vì CQRS không sử dụng những khái quát này, nên nó chỉ có thể thực hiện những gì cần thiết.
Lesair Valmont

@LesairValmont Kho lưu trữ chỉ được coi là CRUD. "Chỉ định nhiều thao tác CRUD" chỉ nên là 4 (hoặc 5 với "danh sách"). Nếu bạn có các mẫu truy cập truy vấn cụ thể hơn thì chúng không nên có trong giao diện kho lưu trữ của bạn. Tôi chưa bao giờ gặp phải vấn đề về các phương thức lưu trữ không sử dụng. Bạn có thể đưa ra một ví dụ không?
Samuel

@Samuel: Tôi nghĩ rằng mô hình kho lưu trữ là hoàn toàn tốt cho các kịch bản nhất định, giống như CQRS. Trên thực tế, trên một ứng dụng lớn, sẽ có một số phần phù hợp nhất sẽ là mẫu kho lưu trữ và các phần khác sẽ được hưởng lợi nhiều hơn bởi CQRS. Nó phụ thuộc vào rất nhiều yếu tố khác nhau, như triết lý tiếp theo là phần ứng dụng đó (ví dụ dựa trên nhiệm vụ (CQRS) so với CRUD (repo)), ORM được sử dụng (nếu có), mô hình hóa miền ( ví dụ DDD). Đối với các danh mục CRUD đơn giản, CQRS hoàn toàn quá mức cần thiết và một số tính năng cộng tác trong thời gian thực (như trò chuyện) sẽ không sử dụng.
Lesair Valmont

10

CQRS là một thứ quản lý dữ liệu hơn là và không có xu hướng chảy quá nhiều vào lớp ứng dụng (hoặc Tên miền nếu bạn thích, vì nó có xu hướng thường được sử dụng nhất trong các hệ thống DDD). Mặt khác, ứng dụng MVC của bạn là một ứng dụng lớp trình bày và nên được phân tách khá tốt khỏi lõi truy vấn / tính bền vững của CQRS.

Một điều đáng chú ý khác (đưa ra so sánh của bạn về Loginphương pháp mặc định và mong muốn về các bộ điều khiển mỏng): Tôi sẽ không chính xác tuân theo các mẫu ASP.NET / mã soạn sẵn mặc định là bất cứ điều gì chúng ta nên lo lắng về các thực tiễn tốt nhất.

Tôi cũng thích bộ điều khiển mỏng, vì chúng rất dễ đọc. Mỗi bộ điều khiển tôi thường có một đối tượng "dịch vụ" mà nó kết hợp với nó về cơ bản xử lý logic theo yêu cầu của bộ điều khiển:

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");
    }
}

Vẫn đủ mỏng, nhưng chúng tôi chưa thực sự thay đổi cách hoạt động của mã, chỉ ủy thác việc xử lý cho phương thức dịch vụ, điều này thực sự không phục vụ mục đích nào ngoài việc làm cho các hành động của bộ điều khiển dễ tiêu hóa.

Hãy nhớ rằng, lớp dịch vụ này vẫn chịu trách nhiệm ủy thác logic cho mô hình / ứng dụng theo yêu cầu, nó thực sự chỉ là một phần mở rộng nhỏ của bộ điều khiển để giữ cho mã gọn gàng. Các phương pháp dịch vụ thường khá ngắn.

Tôi không chắc người hòa giải sẽ làm bất cứ điều gì khác về mặt khái niệm: chuyển một số logic bộ điều khiển cơ bản ra khỏi bộ điều khiển và chuyển sang một nơi khác để xử lý.

. có thể đưa vào để làm phức tạp mã bằng cách làm cho nó trông đơn giản hơn, nhưng đó chỉ là bước đầu tiên của tôi)


5

Tôi thực sự khuyên bạn nên xem bản trình bày NDC của Jimmy Bogard về cách tiếp cận của anh ấy để mô hình hóa các yêu cầu http https://www.youtube.com/watch?v=SUiWfhAhgQw

Sau đó, bạn sẽ có được một ý tưởng rõ ràng về những gì Mediatr được sử dụng cho.

Jimmy không có một sự tuân thủ mù quáng với các mẫu và trừu tượng. Anh ấy rất thực dụng. Mediatr không dọn dẹp các hành động điều khiển. Đối với việc xử lý ngoại lệ, tôi đẩy nó vào một lớp cha gọi là Execute. Vì vậy, bạn kết thúc với một hành động điều khiển rất sạch sẽ.

Cái gì đó như:

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;
}

Cách sử dụng trông hơi giống thế này:

[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);
}

Mong rằng sẽ giúp.


4

Nhiều người (tôi cũng vậy) nhầm lẫn mô hình với một thư viện. CQRS là một mẫu nhưng MediatR là một thư viện mà bạn có thể sử dụng để triển khai mẫu đó

Bạn có thể sử dụng CQRS mà không cần MediatR hoặc bất kỳ thư viện nhắn tin đang xử lý nào và bạn có thể sử dụng MediatR mà không cần CQRS:

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

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

CQS sẽ trông như thế này:

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

Thực tế, bạn không phải đặt tên cho các mô hình đầu vào của mình là "Lệnh" như trên CreateProductCommand. Và đầu vào của truy vấn của bạn "Truy vấn". Lệnh và truy vấn là phương thức, không phải mô hình.

CQRS là về sự phân biệt trách nhiệm (các phương thức đọc phải ở một nơi riêng biệt với các phương thức ghi - bị cô lập). Đó là một phần mở rộng cho CQS nhưng sự khác biệt là trong CQS, bạn có thể đặt các phương thức này trong 1 lớp. (không phân biệt trách nhiệm, chỉ tách lệnh truy vấn). Xem sự tách biệt và tách biệt

Từ https://martinfowler.com/bliki/CQRS.html :

Trung tâm của nó là khái niệm rằng bạn có thể sử dụng một mô hình khác để cập nhật thông tin so với mô hình bạn sử dụng để đọc thông tin.

Có sự nhầm lẫn trong những gì nó nói, nó không phải là về một mô hình riêng biệt cho đầu vào và đầu ra, mà là về sự phân chia trách nhiệm.

CQRS và giới hạn thế hệ id

Có một hạn chế bạn sẽ gặp phải khi sử dụng CQRS hoặc CQS

Về mặt kỹ thuật, trong các lệnh mô tả ban đầu không nên trả về bất kỳ giá trị nào (void) mà tôi thấy ngu ngốc vì không có cách nào dễ dàng để tạo id từ một đối tượng mới được tạo: /programming/4361889/how-to- get-id-in-created-when-application-cqrs .

vì vậy bạn phải tự tạo id mỗi lần thay vì để cơ sở dữ liệu làm việc đó.


Nếu bạn muốn tìm hiểu thêm: https://cqrs.files.wordpress.com/2010/11/cqrs_document.pdf


1
Tôi thách thức sự khẳng định của bạn rằng lệnh của CQRS để duy trì dữ liệu mới trong cơ sở dữ liệu không thể trả về Id mới được tạo bởi cơ sở dữ liệu là "ngu ngốc". Tôi nghĩ rằng đây là một vấn đề triết học. Hãy nhớ nhiều về DDD và CQRS là về tính bất biến của dữ liệu. Khi bạn nghĩ về nó hai lần, bạn bắt đầu nhận ra rằng hành động đơn thuần của việc duy trì dữ liệu là một hoạt động đột biến dữ liệu. Và đó không chỉ là về các ID mới mà còn có thể là các trường chứa đầy dữ liệu mặc định, trình kích hoạt và các tệp lưu trữ có thể thay đổi dữ liệu của bạn.
Lesair Valmont

Chắc chắn bạn có thể gửi một số loại sự kiện như "ItemCreated" với một mục mới làm đối số. Nếu bạn chỉ xử lý giao thức phản hồi yêu cầu và sử dụng CQRS "thật" thì id phải được biết trước để bạn có thể chuyển nó sang một hàm truy vấn riêng - hoàn toàn không có gì sai với điều đó. Trong nhiều trường hợp, CQRS chỉ là quá mức cần thiết. Bạn có thể sống mà không có nó. Không có gì ngoài cách cấu trúc mã của bạn và điều đó phụ thuộc chủ yếu vào giao thức bạn sử dụng.
Konrad

Và bạn có thể đạt được sự bất biến dữ liệu mà không cần CQRS
Konrad
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.