65.9K
CodeProject 正在变化。 阅读更多。
Home

分层架构中的领域验证

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (4投票s)

2015年5月31日

CPOL

7分钟阅读

viewsIcon

20760

downloadIcon

693

分层架构中领域验证的方法。

引言

市面上有许多类型的架构,验证并非一个简单的课题。如何验证、验证什么以及在哪里验证,都没有标准指南。在本文中,我将尽力总结我在分层应用程序中进行业务验证的方法。

范围

本文的代码示例是一个 ASP.NET MVC Web 应用程序,但我认为大部分代码只需稍作调整,就能很好地适用于所有类型的应用程序。

背景

为了理解本文,我假设您对分层架构、典型 Web 应用程序的常见层有一定的基本了解。如果对领域驱动设计(DDD)有所了解会更好。虽然本文内容并没有特别针对 DDD,但我会引用一些有关 DDD 中验证的文章。

系统架构

这是我常用的一个分层架构:

该图不仅展示了概念架构,还展示了不同项目之间的实际依赖关系(即项目在解决方案中如何互相引用)。

以下是演示解决方案中相同的架构布局:

该解决方案包含 6 个项目:

  • DomainValidation.UI.Web:表示层,ASP.NET MVC 5
  • DomainValidation.Service.Contracts:DTO、消息以及应用服务接口
  • DomainValidation.Service:应用服务层的实现
  • DomainValidation.Model:业务模型、存储库接口
  • DomainValidation.Repository.Fake:内存中的存储库实现
  • DomainValidation.Infrastructure.DependencyResolution:控制反转(IoC)容器(Autofac)的注册和设置。

项目按此方式组织的一些原因:

  • 系统遵循领域驱动设计中常见的层次结构:UI -> 应用服务 -> 领域服务 + 模型 -> 存储库
  • 理想情况下,应用服务层(Service 项目)及其以下的层可以被不同类型的 UI 项目(Web、桌面、移动)重用。UI 项目可以自由使用任何适合的表示模式(MVC、MVP、MVVM 等)。应用服务层充当外观,隐藏了下层对 UI 层的复杂性。如图所示,Web 项目只知道服务合约项目和依赖关系解析项目。其他项目是松耦合的,仅在运行时才知道。
  • 正如 DDD 中所说,Model 项目是应用程序的核心。它不依赖于任何其他项目,但被其他项目所依赖。在实际应用程序中,这里应该包含丰富的业务领域逻辑。
  • UI 不依赖于 Model。它只依赖于服务层中的 DTO 和消息合约(我在这里对“合约”一词的用法比较宽泛,与 WCF 服务合约无关)。
  • 由于项目侧重于验证和架构,我只实现了一个简单的内存存储库实现,但该实现可以轻松替换为 Entity Framework/NHibernate/ADO.NET 实现。它唯一的依赖是 Model 项目。
  • 由于组合根(composition root)位于 Web 层,因此 Web 项目可以引用所有其他项目。
  • 系统遵循依赖倒置原则,组合根位于 Web 项目。

执行验证的位置

参考上图的架构图,有几个地方可以进行验证:

UI:特定于 UI 的验证。例如,标准 Web 应用程序通常会在发布到服务器之前进行 JavaScript 验证。ASP.NET MVC 应用程序除了 JavaScript 验证外,还会进行模型状态验证等。这里的验证只是简单地检查某个属性是否缺失、字符串的最大长度等,并且会依赖于 UI 中使用的技术。它不应包含涉及不同业务规则、数据库调用的业务验证。尽管有时为了更好的用户体验,某些应用程序会在此层复制业务逻辑验证,但它不应取代底层实际的业务逻辑验证。

应用服务:在我看来,这是启动与业务相关的验证的最佳位置,原因如下:

  • 放置在此处的验证逻辑可以被不同的 UI 客户端共享,例如 Web UI、移动 UI。
  • 如果您的应用程序使用依赖注入,验证可以很好地融入这里的依赖注入结构。
  • 可以在每次操作执行前进行验证,并在实际更新发生之前。
  • 请注意,我在这里使用了“启动”一词。业务逻辑验证可以在此层启动,但这并不意味着所有验证都必须放在此层。根据我构建验证逻辑的结构化方法,无法将所有与业务相关的验证都推送到应该包含所有业务逻辑的核心层,但我相信我们应该尽量将尽可能多的通用业务逻辑验证推送到核心层。这可以通过将通用验证逻辑移到核心层中的 Specification 类(Specification 模式)来完成。

验证结构

应用程序采用面向操作的验证方法。Jimmy Bogard 的文章《Validation in DDD world》[1] 给了我这个想法。要使操作的验证有意义,它需要了解操作的上下文。例如,对于同一个 Product 实体,Update 操作和 Delete 操作的验证可能完全不同。

在演示解决方案中,我使用 Fluent Validation 库(https://fluentvalidation.codeplex.com/)结合 Autofac 进行依赖注入。下面是验证类结构的示意图:

其中,**IValidatorFactory**、**ValidatorFactorybase**、**IValidator**、**AbstractValidator** 由 FluentValidation 库提供。

**AutofacValidatorFactory** 是我们自定义的验证器工厂类,用于使用 Autofac 解析验证器类的实例。此类只需设置一次并注册到 Autofac IoC 容器,非常简单。

    public class AutofacValidatorFactory : ValidatorFactoryBase
    {
        private readonly IComponentContext _context;

        public AutofacValidatorFactory(IComponentContext context)
        {
            _context = context;
        }

        public override IValidator CreateInstance(Type validatorType)
        {
            return _context.Resolve(validatorType) as IValidator;
        }
    }

因此,对于每种新的验证场景,我们只需要编写一个验证器类来放置该场景的实际验证逻辑。例如,对于 Delete Product 操作,这是我们的验证器类:

    public class DeleteProductValidator : AbstractValidator<DeleteProductRequest>
    {
        public DeleteProductValidator(IProductRepository productRepository)
        {
            Custom(p =>
            {
                var productFromDb = productRepository.Find(p.Id);
                return productFromDb.Price > 200
                    ? new ValidationFailure("Price", "Cannot delete product with price greater than 200")
                    : null;
            });            
        }
    }

在此场景中,我只是虚构了一些业务逻辑规则来演示验证,例如,用户不能删除价格超过 200 的产品。另外请注意,此验证器类可以使用构造函数注入来获取其执行任务所需的任何依赖项(在本例中,它从构造函数获取产品存储库作为依赖项。此依赖项将由 Autofac 自动解析)。

FluentValidation 在验证器和它所验证的实例之间有一个一对一的映射。换句话说,一个验证器只能用于验证一个类。因此,对于每个操作,我有一个约定,即使用一个输入模型类来保存执行该操作的所有输入参数(例如,要删除产品,我将有一个 DeleteProductRequest 输入模型,其中只包含要删除产品的 Id 字段)。

在此应用服务类中使用此验证器:

  • 将验证器工厂、验证器类注册到 IoC 容器。以下是 Autofac 的示例:
builder.RegisterType<AutofacValidatorFactory>().As<IValidatorFactory>().InstancePerRequest();

builder.RegisterType<UpdateProductValidator>().As<IValidator<UpdateProductRequest>>().InstancePerRequest();
builder.RegisterType<DeleteProductValidator>().As<IValidator<DeleteProductRequest>>().InstancePerRequest();
  • 在应用服务类的构造函数中注入 IValidatorFactory
        public ManageProductService(ICategoryRepository categoryRepository, IProductRepository productRepository, IValidatorFactory validatorFactory)
        {
            _categoryRepository = categoryRepository;
            _productRepository = productRepository;
            _validatorFactory = validatorFactory;
        }
  • 在应用服务的每个更新操作中,使用验证器工厂根据输入模型类型获取验证器类的实例,并使用验证器来验证输入。在这种情况下,我选择让 FluentValidation 在任何业务规则被违反时抛出异常。
        public void Delete(DeleteProductRequest request)
        {
            var validator = _validatorFactory.GetValidator<DeleteProductRequest>();
            validator.ValidateAndThrow(request);

            _productRepository.Delete(request.Id);
        }

 

处理业务异常:

如上所示,当任何业务规则被违反时,应用服务将抛出 ValidationException 类型的异常。因此,在 UI 层,它应该捕获此异常并向用户显示适当的响应。以下是 ProductController 中处理异常的 Delete() 操作:

        [HandleBusinessException]
        public ActionResult Delete(DeleteProductRequest request)
        {
            _manageProductService.Delete(request);
            return RedirectToAction("Index")
                .WithSuccess("Product deleted successfully");
        }

这里有两件有趣的事情我想指出:

  • 我创建了一个简单的 HandleBusinessException,它继承自 ASP.NET MVC 的 HandleErrorAttribute。此属性将捕获控制器中的任何 ValidationException,并执行以下操作之一:
    • 如果是一个常规的页面请求,则重定向到控制器中具有业务错误(存储在 TempData 中)的指定操作。
    • 如果是一个 ajax 请求,则返回一个包含 Success 和 Errors 两个字段的 json 对象。

该属性有 3 个可配置参数:ForAjaxRequest、ControllerToRedirectIfInvalid、ActionToRedirectIfInvalid。

  • 控制器使用扩展方法 WithSuccess("message") 来显示一个 Bootstrap 成功提示(或者在 HandleBusinessException 中使用 WithError("error") 来显示错误提示)。在后台,这些扩展方法 WithSuccess()、WithError() 等将创建自定义的 ActionResult,将消息存储在 TempData 中,并在 _Layout.cshtml 页面中渲染。有关如何实现的详细信息,请参见演示解决方案。这是一个我从 Pluralsight 课程“Build Your Own Application Framework with ASP.NET MVC 5”中学到的技巧。

当业务逻辑被违反时,效果如下:

参考文献

[1]: https://lostechies.com/jimmybogard/2009/02/15/validation-in-a-ddd-world/

历史

2015/5/30:初始版本

© . All rights reserved.