【问题标题】:Injecting ModelState into Service将 ModelState 注入服务
【发布时间】:2016-11-04 15:18:47
【问题描述】:

我正在尝试将控制器的 ModelState 注入到我的服务层中,以添加与业务逻辑相关的验证错误(例如条目已存在)。

为此,我创建了一个新的 IValidationDictionary,它通过 Initialize() 函数注入到我的服务中。根本没有什么新鲜事,从我的谷歌搜索来看,有些人在 MVC 中所做的事情。

Controller 构造函数如下所示:

public AccountController(IAccountService accountService)
{
   _accountService = accountService;
   _accountService.Initialize(new ValidationDictionary(ModelState));
}

这一切都很好,我可以在我的服务中添加错误。再次离开服务时出现问题。那时,控制器 ModelState 中不存在我的任何错误。经过一番调试,我发现控制器的构造函数中的ModelState与Action中的不一样。在某些时候,它似乎创建了一个新的 ModelState。

另一种选择似乎是调用 Initialize() 在每个动作开始时注入 ModelState。在我这样做之前,我想问一下是否有人有更优雅的方式(比如更少的类型)来解决这个问题。

编辑: IValidationDictionary: 在业务层:

public interface IValidationDictionary
    {
        void AddError(string key, string message);
        bool IsValid { get; }
    }

在控制器中:

public class ValidationDictionary : IValidationDictionary
    {
        private ModelStateDictionary _modelState;

        public ValidationDictionary(ModelStateDictionary modelState)
        {
            _modelState = modelState;
        }

        public bool IsValid
        {
            get
            {
                return _modelState.IsValid;
            }
        }

        public void AddError(string key, string message)
        {
            _modelState.AddModelError(key, message);
        }
    }

【问题讨论】:

标签: c# asp.net-core asp.net-core-mvc


【解决方案1】:

首先,您不应该这样做,因为您将混合两件事并打破关注点分离并紧密耦合您的应用程序。

紧密耦合

ModelState 属性的类型为 ModelStateDictioanry,它是 ASP.NET Core 特定的类。如果在业务层中使用它,则会创建对 ASP.NET Core 的依赖,从而无法在 ASP.NET Core 之外的任何地方重用您的逻辑,即作为纯控制台应用程序的后台工作进程,因为您既不引用 ASP。 NET Core 也不会有 HttpContext 或其他任何东西。

关注点分离

业务验证和输入验证是两个不同的东西,应该以不同的方式处理。 ModelStateDictionary 用于输入验证,以验证传递给控制器​​的输入。它并不意味着验证业务逻辑或类似的东西!

另一方面,业务验证不仅仅是对字段及其模式的粗略验证。它包含逻辑和验证可能很复杂,并且取决于多个属性/值以及当前对象的状态。因此,例如,可能通过输入验证的值可能会在业务验证中失败。

因此,如果同时使用两者,您将违反关注点分离,并且让一个类做不止一件事。从长远来看,这不利于维护代码。

如何解决?

IDicitionary<string, ModelStateEntry>转换为自定义验证模型/ValidationResult

ValidationResult 在 System.Components.DataAnnotations` 程序集中定义,该程序集不绑定到 ASP.NET Core,而是 .NET Core/完整 .NET Framework 的端口,因此您不会依赖 ASP.NET核心并且可以在控制台应用程序等中重用它,并使用工厂类在您的验证服务中传递它

public interface IAccoutServiceFactory
{
    IAccountService Create(List<ValidationResult> validationResults);
}

// in controller
List<ValidationResult> validationResults = ConvertToValidationResults(ModelState);
IAccountService accountService = accountServiceFactory.Create(

这解决了依赖问题,但您仍然违反了关注点分离,特别是如果您在业务层中使用与控制器参数相同的模型。

自定义验证器(完全分离)

一开始需要做更多的工作,但您的验证将是完全独立的,您可以更改其中一个而不影响另一个。

为此,您可以使用 Fluent Validations 之类的框架,这可能会使验证更容易和更易于管理

您编写了一个自定义验证器,它将验证某个类/模型的逻辑。

自定义验证器可以简单到为每个模型编写自己的验证器,它可以实现这样的接口

public interface IValidator<T> where T : class
{
    bool TryValidate(T model, out List<ValidationErrorModel> validationResults);
    List<ValidationErrorModel> Validate(T model);
}

并将其包裹在您的验证器类中

public class ModelValidator : IModelValidator
{
    public List<ValidationErrorModel> Validate<T>(T model) 
    {
        // provider is a IServiceProvider
        var validator = provider.RequireService(typeof(IValidator<T>));
        return validator.Validate(model);
    }
}

自定义验证器(基于验证属性)

上述方法的替代,但使用验证属性作为基础和自定义逻辑。您可以使用来自System.ComponentModel.DataAnnotationsValidator.TryValidateObject 来验证模型ValidatorAttributes。但请注意,它只会验证传递的模型属性,而不是子模型。

List<ValidationResult> results = new List<ValidationResult>();
var validationContext = new ValidationContext(model);

if(!Validator.TryValidateObject(model, validateContext, results))
{
    // validation failed
}

然后额外执行自定义逻辑。请参阅此blog post,了解如何实现子模型验证。

恕我直言,最简洁的方法是自定义验证器,因为它既分离又解耦,并且允许您轻松更改模型的逻辑而不影响其他模型的验证。

如果您只是验证消息(即 CQRS 中的命令/查询),您可以使用带有验证属性的第二种方法。

【讨论】:

  • 我通过使用 IValidationDictionary 消除了对 ASP.NET 的依赖,但我明白你的观点是分离关注点。在对模型进行任何操作之前,我已经验证了所有属性,但服务会检查其他真正应该由它检查的约束(例如,如果帐户名存在)。我添加了 ValidationDictionary 的代码。
【解决方案2】:

为每个请求创建一个新控制器。根据您将服务配置为瞬态还是单例,每个请求都会创建一个新的IAccountService,或者重用一个实例。由于您管理每个请求的状态,我假设您有一个临时服务,即每个请求都有一个新实例。一旦请求消失,这些实例就会被取消引用。

不过,我不知道你离开服务是什么意思。我希望这提供了正确的输入来追踪您的问题。

【讨论】:

  • 我所说的“离开服务”是指当它返回控制器动作时,所以在服务功能完成后。我测试了 Transient 和 Scoped 服务范围,两者的行为相同。
  • 只要确保你测试并发。使用单例实例,您可以通过请求共享它,并将在同一个实例上多次调用 init,可能同时调用。
  • 初始化只被调用一次。我想我会坚持 Tseng 的建议并为此制作新的验证字典,而不是重新使用 ModelState
【解决方案3】:

有一种更简单的方法可以访问 ModelState。

在启动时配置 IActionContextAccessor:

services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();

并将其注入到服务中:

public class MyService : IMyService
{
    private readonly IActionContextAccessor _actionContextAccessor;

    public MyService(IActionContextAccessor actionContextAccessor)
    {
        _actionContextAccessor = actionContextAccessor;
    }

    public bool IsValid
    {
        get
        {
            return _actionContextAccessor.ActionContext.ModelState.IsValid;
        }
    }
}

请注意,保留对IActionContextAccessor 的引用很重要(原因与IHttpContextAccessor 类似)。

【讨论】:

    【解决方案4】:

    感谢用户 Ruard,这是我使用 .NET Core 5.0 的方法

    在 Startup.cs 中

    public void ConfigureServices(IServiceCollection services)
    {
        ...
    services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
        ...
    }
    

    在控制器中

    AccountService _service;
    
    public AccountController(IActionContextAccessor actionContextAccessor)
    {
          //this.actionContextAccessor = actionContextAccessor;
          _service = new AccountService(actionContextAccessor);
     }
    

    在服务层类中

    public class AccountService
        {    
            private readonly IActionContextAccessor _actionContextAccessor;
            public AccountService(IActionContextAccessor actionContextAccessor)
            {
                _actionContextAccessor = actionContextAccessor;
            }
    
            public void Login(string emailAddress, string password)
            {  
                _actionContextAccessor.ActionContext.ModelState.AddModelError("Email", "Your error message");
            }
        }
    

    在行动中,你使用like

       _service.Login(model.Email, model.Password);
    
      if(!ModelState.IsValid)
          return View(model);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-09-09
      • 2017-08-24
      • 1970-01-01
      • 1970-01-01
      • 2016-05-09
      • 2017-06-27
      • 1970-01-01
      相关资源
      最近更新 更多