緣起
最近在工作上使用了 FluentValidation 套件幫助我做比較彈性化的模型驗證,不過在自訂模型驗證的時,需要借助其他已經定義好的 Service 來取得一些額外的資訊,剛好專案裡面使用到了 Autofac 作為 DI 的容器。
這篇文章會展示如何在 Validator 中注入其他 Service。
環境
- Visual Studio 2017
- ASP.NET MVC 5 專案
套件
- FluentValidation
- FluentValidation.Mvc
- Autofac
- Autofac.Mvc5
情境
在這個 Lab 當中我們要在網頁中顯示 Name, Email 兩個欄位,送出時 Name, Email 要為必填、並且 Name 要等於 4 (但是這個 4 是從 Service 取得的,而不是事先定義好的)
實作
FooService.cs, IFooService.cs
先定義好要被注入到 Validator 的 FooService,這邊簡單做個 GetBar 方法,它會回傳參數的平方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public interface IFooService { int GetBar(int a); }
public class FooService : IFooService { public int GetBar(int a) { return a * a; } }
|
BarModel.cs
BarModel 只是一個單純的 ViewModel 定義如下
1 2 3 4 5 6 7
| [Validator(typeof(BarModelValidator))] public class BarModel { public string Name { get; set; }
public string Email { get; set; } }
|
BarModelValidator.cs
在 BarModelValidator 的建構式當中注入 IFooService,在 Name 的驗證條件當中指定 Name 屬性要等於 fooService.GetBar(2) 的結果。
前面的程式有提到,GetBar() 內會回傳參數的平方,所以我們要讓結果會回傳 4,參數就傳入 2,並且自訂一串中文訊息,表示我們真的有呼叫到 Service
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class BarModelValidator : AbstractValidator<BarModel> { public BarModelValidator(IFooService fooService) { CascadeMode = CascadeMode.StopOnFirstFailure;
RuleFor(x => x.Name).NotNull() .Must(x => x == fooService.GetBar(2).ToString()) .WithMessage(x => $"{nameof(x.Name)} 要等於 {fooService.GetBar(2).ToString()}"); RuleFor(x => x.Email).NotNull().EmailAddress(); } }
|
當然上面這樣寫,預設是不可能可以執行的,因為在 BarModelValidator 的建構式參數的 IFooService 會是 Null,所以我們要讓整個 Autofac 接管整個 Validator 實體的產生,確保它可以在產生的時候幫我們在建構式上面注入我們想要使用的 Service。
MyValidatorFactory.cs
在 FluentValidation 中已經有自己預設的 Factory,用來產生這些 Validator,但是他在這個情境下不夠用,所以我們自己實作一個,把原本的替換掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class MyValidatorFactory : IValidatorFactory { private readonly IComponentContext _container;
public MyValidatorFactory(IComponentContext container) { _container = container; }
public IValidator<T> GetValidator<T>() { return (IValidator<T>)GetValidator(typeof(T)); }
public IValidator GetValidator(Type type) { var genericType = typeof(IValidator<>).MakeGenericType(type);
if (_container.TryResolve(genericType, out var validator)) return (IValidator)validator;
return null; } }
|
我們可以看到整個自訂的 Factory 中的精華就在 _container.TryResolve()
這段,因為我們的目的是將產生 Validator 的工作交給 Autofac 來做,所以就是從現有的容器去搜尋是不是有預期中的 Validator 存在容器裡面,如果有的話就直接回傳。
AutofacConfig.cs
最後就是將容器註冊的不要不要的,把 Service 和 Validator 都註冊進去,確保我們前面的 MyValidatorFactory 找的到這些已註冊過的型別。
最後 FluentValidationModelValidatorProvider.Configure()
再將預設的 Factory 取代為自訂的 MyValidatorFactory。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public static class AutofacConfig { public static void Bootstrap() { var assemblies = Assembly.GetExecutingAssembly();
var builder = new ContainerBuilder();
builder.RegisterControllers(assemblies);
builder.RegisterModule<AutofacWebTypesModule>(); builder.RegisterFilterProvider();
builder.RegisterAssemblyTypes(assemblies) .AsImplementedInterfaces() .InstancePerLifetimeScope();
builder.RegisterAssemblyTypes(assemblies) .Where(x => x.IsClosedTypeOf(typeof(IValidator<>))) .AsImplementedInterfaces() .InstancePerLifetimeScope();
var container = builder.Build(); DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
FluentValidationModelValidatorProvider.Configure(provider => { provider.ValidatorFactory = new MyValidatorFactory(container); }); } }
|
成果
參考資料
原始碼