6

W wersji FluentValidation istnieje rozszerzenie lub inny sposób odroczenia wyboru walidatora podrzędnego w zależności od typu/wartości sprawdzanej właściwości?Odroczyć wybór walidatora podrzędnego w zależności od typu/wartości właściwości

Moja sytuacja polega na tym, że mam klasę powiadomień, którą chcę sprawdzić. Ta klasa ma właściwość Ładunek, który może być jednym z wielu typów Ładunków, np. SmsPayload, EmailPayload itp. Każda z tych podklas ładunków ma swój powiązany walidator np. SmsPayloadValidator i EmailPayloadValidator odpowiednio. Oprócz powyższego, nie ma odniesień z głównej biblioteki (bibliotek podstawowych) do poszczególnych dostawców powiadomień. Zasadniczo oznacza to, że mogę dodawać dostawców w miarę potrzeb i wszystko podłączać za pomocą IoC.

Rozważmy następujące klasy:

public class Notification 
{ 
    public Payload Payload { get; set; } 
    public IEnumerable<string> Details { get; set; } 
} 

public abstract class Payload 
{ 
    public string Message { get; set; } 
    public abstract string Type { get; } 
} 

public class SmsPayload : Payload 
{ 
    public List<string> Numbers { get; set; } 
    public string Region { get; set; } 
    public string Provider { get; set; } 
} 

Jest walidator Powiadamianie i SmsPayloadValidator następująco:

public class NotificationValidator : AbstractValidator<Notification> 
{ 
    public NotificationValidator(IValidator<Payload> payloadValidator) 
    { 
     RuleFor(notification => notification.Payload).NotNull().WithMessage("Payload cannot be null."); 
     RuleFor(notification => notification.Payload).SetValidator(payloadValidator); 
    } 
} 

public class SmsPayloadValidator : AbstractValidator<SmsPayload> 
{ 
    public SmsPayloadValidator() 
    { 
     RuleFor(payload => payload.Provider) 
      .Must(s => !string.IsNullOrEmpty(s)) 
      .WithMessage("Provider is required."); 
     RuleFor(payload => payload.Numbers) 
      .Must(list => list != null && list.Any()) 
      .WithMessage("Sms has no phone numbers specified."); 
     RuleFor(payload => payload.Region) 
      .Must(s => !string.IsNullOrEmpty(s)) 
      .WithMessage("Region is required."); 
    } 
} 

Jak wspomniałem zespół gdzie NotificationValidator jest nie odwoływać zespoły gdzie poszczególne Klasy walidatorów Payload na żywo. Wszystkie okablowanie jest obsługiwane przez Ioc (Simple-Injector dla tego projektu).

Zasadniczo chcę zrobić coś jak następuje - pierwszy rejestrując zwrotnego fabryki w prosty Injector:

container.Register<Func<Payload, IValidator<Payload>>>(() => (payload => 
{ 
    if (payload.GetType() == typeof(SmsPayload)) 
    { 
     return container.GetInstance<ISmsPayloadValidator>(); 
    } 
    else if (payload.GetType() == typeof(EmailPayload)) 
    { 
     return container.GetInstance<IEmailPayloadValidator>(); 
    } 
    else 
    { 
     //something else; 
    } 
})); 

taka, że ​​mogę wybrać odpowiedni walidator następująco:

public class NotificationValidator : AbstractValidator<Notification> 
{ 
    public NotificationValidator(Func<Payload, IValidator<Payload>> factory) 
    { 
     RuleFor(notification => notification.Payload).NotNull().WithMessage("Payload cannot be null."); 
     RuleFor(notification => notification.Payload).SetValidator(payload => factory.Invoke(payload)); 
    } 
} 

Any propozycje? czy jest lepszy sposób na robienie tego, co proponuję? Jeśli nie, rozwiążę repozytorium FluentValidation i prześlę PR.

Odpowiedz

5

Możesz umyć swoje intencje, unikając fabryk. Podczas gdy efekt końcowy jest prawdopodobnie taki sam w tym podejściu, możesz przynajmniej skończyć wstrzykiwaniem IValidator<Payload> bezpośrednio zamiast Func<Payload, IValidator<Payload>>. Wygeneruj klasę o nazwie PolymorphicValidator. Umożliwi to powtarzanie tego wzorca w spójny sposób, a także, jeśli sobie tego życzysz, zapewnienie zastępczego walidatora podstawowego. Jest to w gruncie rzeczy zalecany "wzór złożony" opisany w dokumencie Simple Injector pod numerem here.

public class PolymorphicValidator<T> : AbstractValidator<T> where T : class 
{ 
    private readonly IValidator<T> _baseValidator; 
    private readonly Dictionary<Type, IValidator> _validatorMap = new Dictionary<Type,IValidator>(); 

    public PolymorphicValidator() { } 

    public PolymorphicValidator(IValidator<T> baseValidator) 
    { 
     _baseValidator = baseValidator; 
    } 

    public PolymorphicValidator<T> RegisterDerived<TDerived>(IValidator<TDerived> validator) where TDerived : T 
    { 
     _validatorMap.Add(typeof (TDerived), validator); 
     return this; 
    } 

    public override ValidationResult Validate(ValidationContext<T> context) 
    { 
     var instance = context.InstanceToValidate; 
     var actualType = instance == null ? typeof(T) : instance.GetType(); 
     IValidator validator; 
     if (_validatorMap.TryGetValue(actualType, out validator)) 
      return validator.Validate(context); 
     if (_baseValidator != null) 
      return _baseValidator.Validate(context); 
     throw new NotSupportedException(string.Format("Attempted to validate unsupported type '{0}'. " + 
      "Provide a base class validator if you wish to catch additional types implicitly.", actualType)); 
    } 
} 

Następnie można zarejestrować swój walidator tak (opcjonalnie zapewniając klasy bazowej małego i dodatkowych weryfikatorów klasy dziecko):

container.RegisterSingle<SmsPayloadValidator>(); 
//container.RegisterSingle<EmailPayloadValidator>(); 
container.RegisterSingle<IValidator<Payload>>(() => 
    new PolymorphicValidator<Payload>(/*container.GetInstance<PayloadValidator>()*/) 
     .RegisterDerived(container.GetInstance<SmsPayloadValidator>()) 
     /*.RegisterDerived(container.GetInstance<EmailPayloadValidator>() */); 

stworzy pojedyncza PolymorphicValidator który zawiera singleton weryfikatorów dzieckiem (Singletons są zalecane przez zespół FluentValidation). Możesz teraz wstrzyknąć IValidator<Payload>, jak pokazano w pierwszym przykładzie z numerem NotificationValidator.

public class NotificationValidator : AbstractValidator<Notification> 
{ 
    public NotificationValidator(IValidator<Payload> payloadValidator) 
    { 
     RuleFor(notification => notification.Payload) 
      .NotNull().WithMessage("Payload cannot be null.") 
      .SetValidator(payloadValidator); 
    } 
} 
+0

+1 za użycie kompozytu. Uważam jednak, że twoja implementacja jest bardziej złożona niż potrzebna. Zobacz moją odpowiedź na alternatywne podejście. – Steven

+0

Dzięki za przegraną. Twierdzę jednak, że moje podejście nie jest bardziej "złożone" niż twoje. Bardziej nużący, tak. Być może niepraktyczne, jak wspominasz w swojej odpowiedzi, ale tylko w zależności od intencji realizatora. –

+1

Choć nieco bardziej skomplikowany, jest trochę bardziej wyraźny i zadziałało. Dzięki! –

4

Zgadzam się z odpowiedzią Taylora na temat użycia Złożonego (więc zdecydowanie +1 do tego), ale jego wdrożenie nie jest tak praktyczne. W związku z tym sugeruję nieco inną implementację, nadal korzystając z kompozytu.

Jeśli się nie mylę, Twój kompozyt powinien wyglądać następująco:

public class CompositeValidator<T> : AbstractValidator<T> where T : class 
{ 
    private readonly Container container; 

    public CompositeValidator(Container container) 
    { 
     this.container = container; 
    } 

    public override ValidationResult Validate(T instance) 
    { 
     var validators = this.container.GetAllInstances(instance.GetType()); 

     return new ValidationResult(
      from IValidator validator in validators 
      from error in validator.Validate(instance).Errors 
      select error); 
    } 
} 

Rejestracja powinna być następująca:

// Simple Injector v3.x 
container.RegisterCollection(typeof(IValidator<>), 
    AppDomain.CurrentDomain.GetAssemblies()); 

container.Register(typeof(IValidator<>), 
    typeof(CompositeValidator<>), 
    Lifestyle.Singleton); 

// Simple Injector v2.x 
container.RegisterManyForOpenGeneric(
    typeof(IValidator<>), 
    container.RegisterAll, 
    AppDomain.CurrentDomain.GetAssemblies()); 

container.RegisterOpenGeneric(
    typeof(IValidator<>), 
    typeof(CompositeValidator<>), 
    Lifestyle.Singleton); 

Co się dzieje tutaj jest następujący:

  • Wywołanie RegisterCollection zapewnia, że ​​wszystkie walidatory są zarejestrowane jako kolekcje. Oznacza to, że dla każdego T może występować wiele walidatorów. Na przykład, jeśli twój system ma PayloadValidator i SmsPayloadValidator, rozstrzygnięcie GetAllInstances<IValidator<SmsPayload>> zwróci oba walidatory, ponieważ IValidator<in T> zawiera słowo kluczowe (jest contravariant).
  • Rejestracja rejestruje numer CompositeValidator<T>, który zostanie zwrócony za każde żądane IValidator<T>. Ponieważ Simple Injector differentiates registrations of collections with one-to-one registrations, wstrzyknięcie IValidator<T> zawsze spowoduje wstrzyknięcie złożonego weryfikatora. Ponieważ złożony weryfikator zależy tylko od kontenera, może być zarejestrowany jako singleton.
  • Konsumentom wstrzykuje się CompositeValidator<T> (gdy zależą one od IValidator<T>), a złożony weryfikator poprosi o zestaw walidatorów oparty na dokładnym typie. Tak więc w przypadku, gdy konsument używa numeru IValidator<Payload>, weryfikator złożony określi typ rzeczywisty (na przykład SmsPayload) i zażąda wszystkich możliwych do przypisania weryfikatorów dla tego typu dokładnego i przekaże walidacje do tych typów.
  • Jeśli nie ma walidatorów dla określonego typu, weryfikator złożony automatycznie zwróci prawidłową wartość ValidationResult.
+1

+1 Dla bardziej ogólnego rozwiązania. Możesz mieć niewielką przewagę, ponieważ jesteś programistą frameworka i właśnie się o tym dowiedziałem, gdy czytam to pytanie ... lol. Jedną z rzeczy, na którą zwracają uwagę obserwatorzy, jest to, że rozwiązanie to jest mniej szczegółowe, ale jest również mniej elastyczne niż moje rozwiązanie. Zakłada się, że implementor zawsze chce zarejestrować i użyć wszystkich walidatorów dla określonego typu. Jeśli taka jest twoja intencja, to zdecydowanie jest to podejście nadrzędne. –

+1

@TaylorBuchanan: Warto zauważyć, że dokonałeś pewnych analiz, czytając dokumenty, tylko po to, by odpowiedzieć na to pytanie. Niewiele osób robi to tutaj na Stackoverflow. – Steven

Powiązane problemy