Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
454 views
in Technique[技术] by (71.8m points)

c# - Asp.Net MVC3: Set custom IServiceProvider in ValidationContext so validators can resolve services

Update 18th December 2012

Since this question seems to be getting quite a few views, I should point out that the accepted answer is not the solution I used, but it does provide the links and resources to build a solution, but, to my mind, not the ideal solution. My answer contains replacements for standard parts of the MVC framework; and you should only use those if you are comfortable checking that they still work for future versions (some private code was ripped out of the official sources, because there wasn't enough extensibility in the base classes).

I can confirm, however, that these two classes also work for Asp.Net MVC 4 as well as 3.

It is also possible to repeat a similar implementation for the Asp.Net Web API framework as well, which I have done recently.

End update

I have a type that has a lot of 'standard' validation (required etc) but also a bit of custom validation as well.

Some of this validation requires grabbing hold of a service object and looking up some lower-level (i.e. 'beneath' the Model layer) meta data using one of the other properties as a key. The meta data then controls whether one or more properties are required as well as valid formats for those properties.

To be more concrete - the type is a Card Payment object, simplified to two of the properties in question as follows:

public class CardDetails
{
  public string CardTypeID { get; set; }
  public string CardNumber { get; set; }
}

I then have a service:

public interface ICardTypeService
{
  ICardType GetCardType(string cardTypeID);
}

ICardType then contains different bits of information - the two here that are crucial being:

public interface ICardType
{
  //different cards support one or more card lengths
  IEnumerable<int> CardNumberLengths { get; set; }
  //e.g. - implementation of the Luhn algorithm
  Func<string, bool> CardNumberVerifier { get; set; }
}

My controllers all have the ability to resolve an ICardTypeService using a standard pattern i.e.

 var service = Resolve<ICardTypeService>();

(Although I should mention that the framework behind this call is proprietary)

Which they gain via the use of a common interface

public interface IDependant
{
  IDependencyResolver Resolver { get; set; }
}

My framework then takes care of assigning the most-specific dependency resolver available for the controller instance when it is constructed (either by another resolver, or by the MVC standard controller factory). That Resolve method in the last-but one code block is a simple wrapper around this Resolver member.

So - if I can grab the selected ICardType for the payment that is received from the browser, I can then perform initial checks on card number length etc. The issue is, how to resolve the service from within my override of IsValid(object, ValidationContext) override of ValidationAttribute?

I need to pass through the current controller's dependency resolver to the validation context. I see that ValidationContext both implements IServiceProvider and has an instance of IServiceContainer - so clearly I should be able to create a wrapper for my service resolver that also implements one of those (probably IServiceProvider).

I've already noted that in all places where a ValidationContext is produced by the MVC framework, the service provider is always passed null.

So at what point in the MVC pipeline should I be looking to override the core behaviour and inject my service provider?

I should add that this will not be the only scenario in which I need to do something like this - so ideally I'd like something which I can apply to the pipeline so that all ValidationContexts are configured with the current service provider for the current controller.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

On MVC 5.2, you can leveragesteal @Andras's answer and the MVC source and:

1. Derive a DataAnnotationsModelValidatorEx from DataAnnotationsModelValidator

namespace System.Web.Mvc
{
    // From https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Mvc/DataAnnotationsModelValidator.cs
    // commit 5fa60ca38b58, Apr 02, 2015
    // Only diff is adding of secton guarded by THERE_IS_A_BETTER_EXTENSION_POINT
    public class DataAnnotationsModelValidatorEx : DataAnnotationsModelValidator
    {
        readonly bool _shouldHotwireValidationContextServiceProviderToDependencyResolver;

        public DataAnnotationsModelValidatorEx(
            ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute,
            bool shouldHotwireValidationContextServiceProviderToDependencyResolver=false)
            : base(metadata, context, attribute)
        {
           _shouldHotwireValidationContextServiceProviderToDependencyResolver =
                shouldHotwireValidationContextServiceProviderToDependencyResolver;
        }
    }
}

2. Clone the base impl of public override IEnumerable<ModelValidationResult> Validate(object container)

3. Apply the hack Render the elegant incision after Validate creates the context:-

public override IEnumerable Validate(object container) { // Per the WCF RIA Services team, instance can never be null (if you have // no parent, you pass yourself for the "instance" parameter). string memberName = Metadata.PropertyName ?? Metadata.ModelType.Name; ValidationContext context = new ValidationContext(container ?? Metadata.Model) { DisplayName = Metadata.GetDisplayName(), MemberName = memberName };

#if !THERE_IS_A_BETTER_EXTENSION_POINT
   if(_shouldHotwireValidationContextServiceProviderToDependencyResolver 
       && Attribute.RequiresValidationContext)
       context.InitializeServiceProvider(DependencyResolver.Current.GetService);
#endif
   ValidationResult result = Attribute.GetValidationResult(Metadata.Model, context);
    if (result != ValidationResult.Success)
    {
        // ModelValidationResult.MemberName is used by invoking validators (such as ModelValidator) to
        // construct the ModelKey for ModelStateDictionary. When validating at type level we want to append the
        // returned MemberNames if specified (e.g. person.Address.FirstName). For property validation, the
        // ModelKey can be constructed using the ModelMetadata and we should ignore MemberName (we don't want
        // (person.Name.Name). However the invoking validator does not have a way to distinguish between these two
        // cases. Consequently we'll only set MemberName if this validation returns a MemberName that is different
        // from the property being validated.

       string errorMemberName = result.MemberNames.FirstOrDefault();
        if (String.Equals(errorMemberName, memberName, StringComparison.Ordinal))
        {
            errorMemberName = null;
        }

       var validationResult = new ModelValidationResult
        {
            Message = result.ErrorMessage,
            MemberName = errorMemberName
        };

       return new ModelValidationResult[] { validationResult };
    }

   return Enumerable.Empty<ModelValidationResult>();
}

4. Tell MVC about the new DataAnnotationsModelValidatorProvider in town

after your Global.asax does DependencyResolver.SetResolver(new AutofacDependencyResolver(container)) :-

DataAnnotationsModelValidatorProvider.RegisterAdapterFactory(
    typeof(ValidatorServiceAttribute),
    (metadata, context, attribute) => new DataAnnotationsModelValidatorEx(metadata, context, attribute, true));

5. Use your imagination to abuse your new Service Locator consume using ctor injection via GetService in your ValidationAttribute, for example:

public class ValidatorServiceAttribute : ValidationAttribute
{
    readonly Type _serviceType;

    public ValidatorServiceAttribute(Type serviceType)
    {
        _serviceType = serviceType;
    }

    protected override ValidationResult IsValid(
        object value, 
        ValidationContext validationContext)
    {
        var validator = CreateValidatorService(validationContext);
        var instance = validationContext.ObjectInstance;
        var resultOrValidationResultEmpty = validator.Validate(instance, value);
        if (resultOrValidationResultEmpty == ValidationResult.Success)
            return resultOrValidationResultEmpty;
        if (resultOrValidationResultEmpty.ErrorMessage == string.Empty)
            return new ValidationResult(ErrorMessage);
        return resultOrValidationResultEmpty;
    }

    IModelValidator CreateValidatorService(ValidationContext validationContext)
    {
        return (IModelValidator)validationContext.GetService(_serviceType);
    }
}

Allows you to slap it on your model:-

class MyModel 
{
    ...
    [Required, StringLength(42)]
    [ValidatorService(typeof(MyDiDependentValidator), 
        ErrorMessage = "It's simply unacceptable")]
    public string MyProperty { get; set; }
    ....
}

which wires it to a:

public class MyDiDependentValidator : Validator<MyModel>
{
    readonly IUnitOfWork _iLoveWrappingStuff;

    public MyDiDependentValidator(IUnitOfWork iLoveWrappingStuff)
    {
        _iLoveWrappingStuff = iLoveWrappingStuff;
    }

    protected override bool IsValid(MyModel instance, object value)
    {
        var attempted = (string)value;
        return _iLoveWrappingStuff.SaysCanHazCheez(instance, attempted);
    }
}

The preceding two are connected by:

interface IModelValidator
{
    ValidationResult Validate(object instance, object value);
}

public abstract class Validator<T> : IModelValidator
{
    protected virtual bool IsValid(T instance, object value)
    {
        throw new NotImplementedException(
            "TODO: implement bool IsValid(T instance, object value)" +
            " or ValidationResult Validate(T instance, object value)");
    }

    protected virtual ValidationResult Validate(T instance, object value)
    {
        return IsValid(instance, value) 
            ? ValidationResult.Success 
            : new ValidationResult("");
    }

    ValidationResult IModelValidator.Validate(object instance, object value)
    {
        return Validate((T)instance, value);
    }
}

I'm open to corrections, but most of all, ASP.NET team, would you be open to a PR to add a constructor with this facility to DataAnnotationsModelValidator?


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...