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

一种可能的解决方案是返回a CommandResult<T>而不是a model,然后CommandResult在后操作过滤器中处理。正如这里所讨论的。

的一种实现CommandResult可能是这样的

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

资源

但这并不能真正解决我们的问题Login,因为有多个故障状态。我们可以将这些额外的失败状态添加到其中,ICommandResult但这对于非常膨胀的类/接口来说是一个很好的开始。有人可能会说它不符合单一职责(SRP)。

另一个问题是returnUrl。我们有这段return RedirectToLocal(returnUrl);代码。我们需要以某种方式基于命令的成功状态来处理条件参数。虽然我认为可以做到(我不确定ModelBinder是否可以将FromBody和FromQuery(returnUrl是FromQuery)参数映射到单个模型)。只能想知道会发生什么疯狂的情况。

随着返回错误消息,模型验证也变得更加复杂。以这个为例

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

我们将错误消息与模型一起附加。这种事情不能用做Exception战略(如建议在这里),因为我们需要的型号。也许您可以从中获取模型,Request但这将是一个非常复杂的过程。

因此,总的来说,我很难转换这种“简单”的动作。

我在寻找输入。我在这里完全错了吗?


6
听起来您已经很了解相关问题。那里有很多“银子弹”,上面有一些玩具实例证明了它们的有用性,但是当它们被实际的,现实的应用程序的现实所挤压时,不可避免地会掉下来。
罗伯特·哈维

查看MediatR行为。从根本上讲,这是一个管道,可让您解决跨领域的问题。
fml

Answers:


14

我认为您期望使用的模式太多。CQRS专为解决查询和数据库命令之间模型上的差异而设计,MediatR只是进程内消息传递库。CQRS并未声称消除了您期望的对业务逻辑的需求。CQRS是数据访问的一种模式,但是您的问题在于表示层(重定向,视图,控制器)。

我认为您可能将CQRS模式错误地应用于身份验证。使用登录时,不能将其建模为CQRS中的命令,因为

命令:更改系统状态但不返回值-Martin
Fowler CommandQuerySeparation

在我看来,身份验证对于CQR​​S来说是一个很差的领域。通过身份验证,您需要高度一致的同步请求-响应流,因此您可以1.检查用户的凭据2.为用户创建会话3.处理您已经确定的各种边缘情况4.立即授予或拒绝用户作为回应。

开发ASP.NET应用程序时值得使用CQRS / MediatR吗?

CQRS是一种具有非常特定用途的模式。目的是为查询和命令建模,而不是像在CRUD中使用的那样为记录建模。随着系统变得越来越复杂,视图的需求通常比仅显示一条记录或少量记录更为复杂,并且查询可以更好地为应用程序需求建模。类似地,命令可以代表对许多记录的更改,而不是代表更改单个记录的CRUD。马丁·福勒警告

像任何模式一样,CQRS在某些地方很有用,但在其他地方则没有用。许多系统确实适合CRUD思维模型,因此应采用这种风格。CQRS对于所有相关人员来说都是一次重大的精神飞跃,因此除非收益值得实现,否则不应该解决。尽管我成功地使用了CQRS,但到目前为止,我遇到的大多数情况都不是很好,因为CQRS被视为使软件系统陷入严重困境的重要力量。
-马丁·福勒CQRS

因此,在适合CRUD的情况下设计应用程序时,回答您的问题CQRS不应该是第一选择。您的问题没有任何迹象表明我有理由使用CQRS。

至于MediatR,它是一个进程内消息传递库,旨在将请求与请求处理分离。您必须再次决定使用该库是否会改善您的设计。我个人不主张进程内消息传递。松耦合可以通过比消息传递更简单的方式实现,我建议您从那里开始。


1
我100%同意。CQRS只是有点炒作,所以我认为“他们”看到了我没有看到的东西。因为我很难看到CRUD Web应用程序中CQRS的好处。到目前为止,唯一可行的方案是CQRS + ES。
Snæbjørn

我新工作中的某个人决定将MediatR放在新的ASP.Net系统上,声称它是一种体系结构。他所做的实现既不是DDD,也不是SOLID,也不是DRY,也不是KISS。这是一个充满YAGNI的小型系统。在包括您在内的一些评论之后,它就已经开始了。我试图弄清楚如何逐渐修改代码以适应其体系结构。我对业务层之外的CQRS持相同的观点,我很高兴有几个经验丰富的开发人员以这种方式思考。
MFedatto

肯定地说,合并CQRS / MediatR的想法可能与很多YAGNI和缺乏KISS有关,但实际上一些流行的替代方法(如存储库模式)通过膨胀存储库类并强迫YAGNI来提升YAGNI有点讽刺意味。接口在要实现此类接口的所有根聚合上指定很多CRUD操作,通常使这些方法未使用或充满“未实现”异常。由于CQRS不使用这些概括,因此只能实现所需的内容。
Lesair Valmont,

@LesairValmont存储库仅应为CRUD。“指定很多CRUD操作”应仅为4(或“列表”为5)。如果您有更具体的查询访问模式,则不应将其放在存储库界面中。我从来没有遇到未使用的存储库方法的问题。你能给我举个例子吗?
塞缪尔

@Samuel:我认为存储库模式在某些情况下非常合适,就像CQRS一样。实际上,在大型应用程序上,将有一些部分最适合存储库模式,而其他一些则可以从CQRS中受益。它取决于许多不同的因素,例如应用程序那部分所遵循的理念(例如,基于任务的(CQRS)与CRUD(repo)),所使用的ORM(如果有),域的建模(例如DDD)。对于简单的CRUD目录,CQRS绝对是过大的,并且某些实时协作功能(例如聊天)不会使用。
Lesair Valmont

10

CQRS不仅仅是数据管理,而且不会渗入应用程序层(如果愿意,也可以渗入到应用层,因为它倾向于在DDD系统中最常用)。另一方面,您的MVC应用程序是表示层应用程序,应该与CQRS的查询/持久性核心区分开。

另一件事值得注意(鉴于您对默认Login方法和对瘦控制器的期望的比较):我不会完全遵循默认的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");
    }
}

仍然很薄,但是我们并没有真正改变代码的工作方式,只是将处理委托给service方法,除了使控制器动作易于理解之外,它实际上没有其他目的。

请记住,该服务类仍然负责根据需要将逻辑委派给模型/应用程序,它实际上只是对控制器的一点扩展,以保持代码整洁。服务方法通常也很短。

我不确定调解器在概念上是否会做任何与之不同的事情:将一些基本的控制器逻辑移出控制器并移至其他地方进行处理。

(我以前没有听说过MediatR,快速浏览github页面似乎并不表明它具有开创性-肯定不像CQRS-实际上,它看起来就像是另一个抽象层可以通过使其看起来更简单来使代码复杂化,但这只是我的初衷)


5

我强烈建议您查看Jimmy Bogard的NDC演示文稿,了解他对HTTP请求建模的方法https://www.youtube.com/watch?v=SUiWfhAhgQw

然后,您将清楚了解Mediatr的用途。

吉米(Jimmy)对样式和抽象没有盲目的依从。他很务实。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。并输入您的查询“查询”。命令和查询是方法,而不是模型。

CQRS是关于责任分离的(读取方法必须与写入方法分开放置-隔离)。它是对CQS的扩展,但不同之处在于您可以将这些方法放在1类中。(没有责任分离,只是命令查询分离)。参见分离与隔离

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

其核心思想是,您可以使用与用于读取信息的模型不同的模型来更新信息。

它说的话很混乱,不是关于输入和输出的单独模型,而是关于责任分离。

CQRS和ID生成限制

使用CQRS或CQS时将面临一个限制

从技术上讲,在原始描述中,命令不应返回我认为很愚蠢的任何值(无效),因为没有简单的方法可从新创建的对象获取生成的ID:https : //stackoverflow.com/questions/4361889/how-to-应用ID时创建ID

因此,您每次必须自己生成ID,而不是让数据库来生成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只是过分杀伤力。你可以没有它。它不过是结构化代码的一种方式,并且在很大程度上还取决于您所使用的协议。
康拉德

您还可以实现数据的不变性不CQRS
康拉德
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.