如何在ASP.NET Core 2 MVC中实现可单元测试的依赖注入式模型验证
在ASP.NET Core 2 MVC中实现带可注入依赖的可测试模型验证器
问题描述
我正在开发ASP.NET Core 2 MVC应用,需要借助依赖项验证用户输入,而且希望验证逻辑能做单元测试,还能注入模拟依赖。之前在MVC5里我用自定义验证器和提供器的方式实现了,但到Core里找不到对应的ModelValidator类,现在搜到的方案要么是在控制器里注入依赖,要么是在IValidatableObject的Validate方法里手动解析依赖,都不是我想要的。
先贴一下我MVC5里的实现,方便大家理解需求:
// 待验证的视图模型 public class MyViewModel { public string Username { get; set; } } // 带依赖注入的模型验证器 public class MyViewModelValidator : ModelValidator { private IUserService users; private MyViewModel model; public MyViewModelValidator(ModelMetadata metadata, ControllerContext controllerContext, IUserService users) : base(metadata, controllerContext) { this.users = users; this.model = base.Metadata.Model as MyViewModel; } public override IEnumerable<ModelValidationResult> Validate(object container) { List<ModelValidationResult> errors = new List<ModelValidationResult>(); if (this.users.CheckExists(this.model.Username)) { errors.Add(new ModelValidationResult() { MemberName = nameof(MyViewModel.Username), Message = "Username is not available" }); } return errors; } } // 匹配模型与验证器的提供器,用Unity注入依赖 public class ViewModelValidatorProvider : ModelValidatorProvider { private IUnityContainer container; public ViewModelValidatorProvider() => this.container = DependencyResolver.Current.GetService<IUnityContainer>(); public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context) { if (metadata.ModelType == typeof(MyViewModel)) yield return new MyViewModelValidator(metadata, context, this.container.Resolve<IUserService>()); } } // 启动时注册提供器 public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { ModelValidatorProviders.Providers.Add(new ViewModelValidatorProvider()); } }
想问问在ASP.NET Core 2里能不能实现类似的功能?
解决方案
当然可以!ASP.NET Core MVC里用IModelValidator和IModelValidatorProvider这两个接口替代了MVC5里的ModelValidator和ModelValidatorProvider,而且原生支持依赖注入,完全不需要第三方容器,就能实现可测试的验证逻辑。
下面一步步来实现:
1. 编写自定义IModelValidator
这个类负责具体的验证逻辑,直接通过构造函数注入依赖,非常直观:
public class MyViewModelValidator : IModelValidator { private readonly IUserService _userService; // 构造函数注入业务服务 public MyViewModelValidator(IUserService userService) { _userService = userService; } public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context) { var model = context.Model as MyViewModel; if (model == null) { yield break; } // 调用依赖服务验证用户名是否存在 if (_userService.CheckExists(model.Username)) { yield return new ModelValidationResult { MemberName = nameof(MyViewModel.Username), Message = "Username is not available" }; } } }
2. 实现IModelValidatorProvider
这个类的作用是给指定模型分配对应的验证器,而且可以利用Core的DI容器来解析验证器的依赖:
public class ViewModelValidatorProvider : IModelValidatorProvider { private readonly IServiceProvider _serviceProvider; // 注入IServiceProvider来获取所需的验证器实例 public ViewModelValidatorProvider(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public void CreateValidators(ModelValidatorProviderContext context) { // 只给MyViewModel添加自定义验证器 if (context.ModelMetadata.ModelType == typeof(MyViewModel)) { var validator = _serviceProvider.GetRequiredService<MyViewModelValidator>(); context.Validators.Add(validator); } } }
3. 在Startup里注册服务
打开Startup.cs,在ConfigureServices方法里把我们的验证器、提供器还有业务服务都注册到DI容器:
public void ConfigureServices(IServiceCollection services) { // 注册你的业务服务,这里用Scoped生命周期,根据实际情况调整 services.AddScoped<IUserService, UserService>(); // 注册自定义验证器 services.AddScoped<MyViewModelValidator>(); // 注册自定义验证器提供器 services.AddTransient<IModelValidatorProvider, ViewModelValidatorProvider>(); // 注册MVC服务 services.AddMvc(); }
4. 单元测试验证器
现在你可以轻松用模拟依赖来测试验证逻辑了,比如用Moq框架:
[TestClass] public class MyViewModelValidatorTests { [TestMethod] public void Validate_WhenUsernameExists_ReturnsValidationError() { // 准备模拟服务 var mockUserService = new Mock<IUserService>(); mockUserService.Setup(s => s.CheckExists("takenUsername")).Returns(true); // 初始化验证器 var validator = new MyViewModelValidator(mockUserService.Object); var testModel = new MyViewModel { Username = "takenUsername" }; // 创建验证上下文 var metadataProvider = new EmptyModelMetadataProvider(); var modelMetadata = metadataProvider.GetMetadataForType(typeof(MyViewModel)); var actionContext = new ActionContext(); var validationContext = new ModelValidationContext(actionContext, modelMetadata, new Dictionary<object, object>(), testModel); // 执行验证 var validationResults = validator.Validate(validationContext).ToList(); // 断言结果 Assert.AreEqual(1, validationResults.Count); Assert.AreEqual(nameof(MyViewModel.Username), validationResults[0].MemberName); Assert.AreEqual("Username is not available", validationResults[0].Message); } }
方案优势
- 完全基于ASP.NET Core原生DI,不需要第三方容器,减少额外依赖
- 验证器的依赖注入符合构造函数注入的最佳实践,代码更清晰
- 单元测试非常方便,直接实例化验证器并传入模拟依赖即可,不需要启动整个Web环境
内容的提问来源于stack exchange,提问作者Ben
相关产品推荐
相关产品推荐

