如何在 FluentValidation 的 Validator 中注入自訂的 Service

Posted by Ryan Tseng on 2017-11-08

緣起

最近在工作上使用了 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
// IFooService.cs
public interface IFooService
{
int GetBar(int a);
}

// FooService.cs
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);

// 從現有的容器中搜尋是否有預期中的 Validator
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();

// 註冊 (會包含 Service)
builder.RegisterAssemblyTypes(assemblies)
.AsImplementedInterfaces()
.InstancePerLifetimeScope();

// 註冊 Validator (只有註冊 Validator)
builder.RegisterAssemblyTypes(assemblies)
.Where(x => x.IsClosedTypeOf(typeof(IValidator<>)))
.AsImplementedInterfaces()
.InstancePerLifetimeScope();

var container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));

// 將 ValidatorProvider 取代為自訂的 Factory
FluentValidationModelValidatorProvider.Configure(provider =>
{
provider.ValidatorFactory = new MyValidatorFactory(container);
});
}
}

成果

參考資料

原始碼