问题描述
我试图将控制器的ModelState注入我的Service层,以添加与业务逻辑相关的验证错误(例如,条目已存在).
为此,我创建了一个新的IValidationDictionary,它通过Initialize()函数注入到我的服务中.根本没有什么新鲜的东西,通过我的Google判断,搜索某些人在MVC中所做的事情.
Controller构造函数如下:
公共AccountController(IAccountService accountService){_accountService = accountService;_accountService.Initialize(new ValidationDictionary(ModelState));}
一切正常,我可以在服务中添加错误.再次离开服务时出现问题.到那时,我的所有错误都没有出现在控制器ModelState中.经过一些调试后,我发现控制器的构造函数中的ModelState与Action中的ModelState不同.在某个时候,它似乎创建了一个新的ModelState.
一种替代方法似乎是在每个Action开始时调用Initialize()来注入ModelState.在我这样做之前,我想问问是否有人能以更优雅的方式来解决这个问题.
IValidationDictionary:在商务层上:
公共接口IValidationDictionary{void AddError(字符串键,字符串消息);bool IsValid {get;}}
在控制器中:
公共类ValidationDictionary:IValidationDictionary{private ModelStateDictionary _modelState;公共ValidationDictionary(ModelStateDictionary modelState){_modelState = modelState;}公共布尔IsValid{得到{返回_modelState.IsValid;}}公共无效AddError(字符串键,字符串消息){_modelState.AddModelError(key,message);}}
首先,您不应该这样做,因为您将混合两件事并打破关注点分离并紧密耦合您的应用程序.
紧密耦合
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 Core,并且可以在控制台应用程序等中重用它,并使用工厂类将其传递到您的验证服务中
公共接口IAccoutServiceFactory{IAccountService创建(List< ValidationResult> validationResults);}//在控制器中List< ValidationResult>validateResults = ConvertToValidationResults(ModelState);IAccountService accountService = accountServiceFactory.Create(
这解决了依赖性问题,但是您仍然违反关注点分离的问题,尤其是如果您在业务层中使用与用作控制器参数相同的模型时.
自定义验证码(完全分隔)
一开始它需要做更多的工作,但是您的验证将是完全独立的,您可以更改其中一个而不影响另一个.
为此,您可以使用Fluent Validations之类的框架,该框架可以使验证更容易,更易于管理
您编写了一个自定义验证器,它将验证某个类/模型的逻辑.
自定义验证器可以很简单,只要为每个模型编写自己的验证器即可,这样就可以实现这样的接口
公共接口IValidator< T>其中T:类{bool TryValidate(T模型,out List< ValidationErrorModel> validationResults);List< ValidationErrorModel>验证(T模型);}
并将其包装在您的验证器类中
公共类ModelValidator:IModelValidator{公共列表< ValidationErrorModel>验证T(T模型){//提供者是IServiceProvidervar validateator = provider.RequireService(typeof(IValidator< T>)));返回validator.Validate(模型);}}
自定义验证(基于验证属性)
上述内容的替代形式,但使用验证属性作为基础和自定义逻辑.您可以使用 System.ComponentModel.DataAnnotations
中的 Validator.TryValidateObject
来验证模型 ValidatorAttributes
.但是请注意,它只会验证传递的模型属性,而不会验证子模型.
List< ValidationResult>结果=新的List< ValidationResult>();varvalidationContext =新的ValidationContext(model);if(!Validator.TryValidateObject(model,validateContext,results)){//验证失败}
,然后另外执行自定义逻辑.请参阅此博客文章有关如何实施子项的信息模型验证.
最简单的方法是自定义验证器,因为它既分离又解耦,并且可以轻松地更改模型的逻辑,而不会影响其他模型的验证.
如果您仅验证消息(即CQRS中的命令/查询),则可以将第二种方法与验证属性一起使用.
I am trying to inject the ModelState of the controller into my Service layer to add business logic related validation errors (e.g. entry already exists).
For this I created a new IValidationDictionary that gets injected into my service through an Initialize() function. Nothing new at all and judging by my google searches something quite some people do in MVC.
The Controller constructor looks like this:
public AccountController(IAccountService accountService)
{
_accountService = accountService;
_accountService.Initialize(new ValidationDictionary(ModelState));
}
This all works fine and I can add errors in my service. The issue comes when leaving the service again. At that point none of my errors are present in the controller ModelState. After some debugging I found out that the ModelState in the constructor of the controller is not the same as in the Action. At some point it seems to create a new ModelState.
One alternative seems to be to call the Initialize() to inject the ModelState at start of every Action. Before I do that, I wanted to ask if anyone has a more elegant way (as in less to type) of solving this.
Edit:The IValidationDictionary:On buisness layer:
public interface IValidationDictionary
{
void AddError(string key, string message);
bool IsValid { get; }
}
In controller:
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);
}
}
First and foremost, you shouldn't do that because you will be mixing two things and break separation of concerns and tightly-couple your application.
Tightly coupling
ModelState
property is of type ModelStateDictioanry
which is a ASP.NET Core specific class. If you use it in your business layer, you create a dependency on ASP.NET Core, making it impossible to reuse your logic anywhere outside of ASP.NET Core, i.e. Background Worker process which is a pure console application because you neither reference ASP.NET Core nor you'll have HttpContext or anything else there.
Separation of concerns
Business validation and input validation are two different things and should be handled differently. ModelStateDictionary
is used for input validation, to validate the input passed to your controller. It is not meant to validate business logic or anything like that!
Business validation on the other side is more than just a rough validation of the fields and its patterns. It contains logic and validation may be complex and depend on multiple properties/values as well as of the state of the current object. So for example, values that may pass the input validation may fail in business validation.
So by using both together, you will violate separation of concerns and have a class do more than one thing. This is bad for maintaining code in the long run.
How to work around it?
Convert IDicitionary<string, ModelStateEntry>
to a custom validation model/ValidationResult
ValidationResult
is defined in System.Components.DataAnnotations` assembly which is not tied to ASP.NET Core but is port of .NET Core/full .NET Framework, so you don't get a dependency on ASP.NET Core and can reuse it in console applications etc. and pass it around in your validation service using i.e. a factory class
public interface IAccoutServiceFactory
{
IAccountService Create(List<ValidationResult> validationResults);
}
// in controller
List<ValidationResult> validationResults = ConvertToValidationResults(ModelState);
IAccountService accountService = accountServiceFactory.Create(
This solves the issue of dependencies, but you still violate separation of concerns, especially if you use the same model in your business layer as you use as controller parameter.
Custom validatior (completely separated)
Its bit more work at the beginning, but your validation will be completely independent and you can change one of it w/o affecting the other one.
For it you can use frameworks like Fluent Validations which may make the validation a bit easier and more managed
You write a custom validator, that will validate the logic of a certain class/model.
The custom validator can be from as simple as writing your own validators per model which may implement such an interface
public interface IValidator<T> where T : class
{
bool TryValidate(T model, out List<ValidationErrorModel> validationResults);
List<ValidationErrorModel> Validate(T model);
}
and wrap this around your validator class
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);
}
}
Custom validatior (validation attribute based)
An alternation of the above, but using the validation attributes as base and custom logic. You can use Validator.TryValidateObject
from System.ComponentModel.DataAnnotations
to validate a models ValidatorAttributes
. Be aware though, that it only will validate the passed models attributes and not of child models.
List<ValidationResult> results = new List<ValidationResult>();
var validationContext = new ValidationContext(model);
if(!Validator.TryValidateObject(model, validateContext, results))
{
// validation failed
}
and then additionally perform custom logic. See this blog post on how to implement child model validation.
Imho the cleanest way to do is a custom validator, as its both separated and decoupled and easily allows you to change logic of a model w/o affecting the validation of other models.
If you are only validating messages (i.e. commands/queries in CQRS) you can use the second approach with the validation attributes.
这篇关于将ModelState注入服务的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!