可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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 ValidationContext
s are configured with the current service provider for the current controller.
回答1:
Have you thought about creating a model validator, using a modelValidatorProvider, instead of using validation attributes? This way you're not dependant on ValidationAttribute but can create your own validation implementation (this will work in addition the existing DataAnnotations validation).
http://msdn.microsoft.com/en-us/library/system.web.mvc.modelvalidatorprovider.aspx
http://dotnetslackers.com/articles/aspnet/Experience-ASP-NET-MVC-3-Beta-the-New-Dependency-Injection-Support-Part2.aspx#s10-new-support-for-validator-provider
http://dotnetslackers.com/articles/aspnet/Customizing-ASP-NET-MVC-2-Metadata-and-Validation.aspx#s2-validation
回答2:
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
?
回答3:
Update
In addition to the class shown below, I've done a similar thing for IValidatableObject
implementations as well (short notes towards the end of the answer instead of a full code sample because then the answer just gets too long) - I've added the code for that class as well in response to a comment - it does make the answer very long, but at least you'll have all the code you need.
Original
Since I'm targeting ValidationAttribute
-based validation at the moment I researched where MVC creates the ValidationContext
that gets fed to the GetValidationResult
method of that class.
Turns out it's in the DataAnnotationsModelValidator
's Validate
method:
public override IEnumerable<ModelValidationResult> 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).
ValidationContext context = new ValidationContext(
container ?? Metadata.Model, null, null);
context.DisplayName = Metadata.GetDisplayName();
ValidationResult result =
Attribute.GetValidationResult(Metadata.Model, context);
if (result != ValidationResult.Success) {
yield return new ModelValidationResult {
Message = result.ErrorMessage
};
}
}
(Copied and reformatted from MVC3 RTM Source)
So I figured some extensibility here would be in order:
public class DataAnnotationsModelValidatorEx : DataAnnotationsModelValidator
{
public DataAnnotationsModelValidatorEx(
ModelMetadata metadata,
ControllerContext context,
ValidationAttribute attribute)
: base(metadata, context, attribute)
{
}
public override IEnumerable<ModelValidationResult> Validate(object container)
{
ValidationContext context = CreateValidationContext(container);
ValidationResult result =
Attribute.GetValidationResult(Metadata.Model, context);
if (result != ValidationResult.Success)
{
yield return new ModelValidationResult
{
Message = result.ErrorMessage
};
}
}
// begin Extensibility
protected virtual ValidationContext CreateValidationContext(object container)
{
IServiceProvider serviceProvider = CreateServiceProvider(container);
//TODO: add virtual method perhaps for the third parameter?
ValidationContext context = new ValidationContext(
container ?? Metadata.Model,
serviceProvider,
null);
context.DisplayName = Metadata.GetDisplayName();
return context;
}
protected virtual IServiceProvider CreateServiceProvider(object container)
{
IServiceProvider serviceProvider = null;
IDependant dependantController =
ControllerContext.Controller as IDependant;
if (dependantController != null && dependantController.Resolver != null)
serviceProvider = new ResolverServiceProviderWrapper
(dependantController.Resolver);
else
serviceProvider = ControllerContext.Controller as IServiceProvider;
return serviceProvider;
}
}
So I check first for my IDependant
interface from the controller, in which case I create an instance of a wrapper class that acts as an adapter between my IDependencyResolver
interface and System.IServiceProvider
.
I thought I'd also handle cases where a controller itself is an IServiceProvider
too (not that that applies in my case - but it's a more general solution).
Then I make the DataAnnotationsModelValidatorProvider
use this validator by default, instead of the original:
//register the new factory over the top of the standard one.
DataAnnotationsModelValidatorProvider.RegisterDefaultAdapterFactory(
(metadata, context, attribute) =>
new DataAnnotationsModelValidatorEx(metadata, context, attribute));
Now 'normal' ValidationAttribute
-based validators, can resolve services:
public class ExampleAttribute : ValidationAttribute
{
protected override ValidationResult
IsValid(object value, ValidationContext validationContext)
{
ICardTypeService service =
(ICardTypeService)validationContext.GetService(typeof(ICardTypeService));
}
}
This still leaves direct ModelValidator
-derived needing to be reimplemented to support the same technique - although they already have access to the ControllerContext
, so it's less of an issue.
Update
A similar thing has to be done if you want IValidatableObject
-implementing types to be able to resolve services during the implementation of Validate
without having to keep deriving your own adapters for each type.
- Derive a new class from
ValidatableObjectAdapter
, I called it ValidatableObjectAdapterEx
- from MVCs v3 RTM source, copy the
Validate
and ConvertResults
private method of that class.
- Adjust the first method to remove references to internal MVC resources, and
- change how the
ValidationContext
is constructed
Update (in response to comment below)
Here's the code for the ValidatableObjectAdapterEx
- and I'll point out hopefully more clearly that IDependant
and ResolverServiceProviderWrapper
used here and before are types that only apply to my environment - if you're using a global, statically-accessible DI container, however, then it should be trivial to re-implement these two classes' CreateServiceProvider
methods appropriately.
public class ValidatableObjectAdapterEx : ValidatableObjectAdapter
{
public ValidatableObjectAdapterEx(ModelMetadata metadata,
ControllerContext context)
: base(metadata, context) { }
public override IEnumerable<ModelValidationResult> Validate(object container)
{
object model = base.Metadata.Model;
if (model != null)
{
IValidatableObject instance = model as IValidatableObject;
if (instance == null)
{
//the base implementation will throw an exception after
//doing the same check - so let's retain that behaviour
return base.Validate(container);
}
/* replacement for the core functionality */
ValidationContext validationContext = CreateValidationContext(instance);
return this.ConvertResults(instance.Validate(validationContext));
}
else
return base.Validate(container); /*base returns an empty set
of values for null. */
}
/// <summary>
/// Called by the Validate method to create the ValidationContext
/// </summary>
/// <param name="instance"></param>
/// <returns></returns>
protected virtual ValidationContext CreateValidationContext(object instance)
{
IServiceProvider serviceProvider = CreateServiceProvider(instance);
//TODO: add virtual method perhaps for the third parameter?
ValidationContext context = new ValidationContext(
instance ?? Metadata.Model,
serviceProvider,
null);
return context;
}
/// <summary>
/// Called by the CreateValidationContext method to create an IServiceProvider
/// instance to be passed to the ValidationContext.
/// </summary>
/// <param name="container"></param>
/// <returns></returns>
protected virtual IServiceProvider CreateServiceProvider(object container)
{
IServiceProvider serviceProvider = null;
IDependant dependantController = ControllerContext.Controller as IDependant;
if (dependantController != null && dependantController.Resolver != null)
{
serviceProvider =
new ResolverServiceProviderWrapper(dependantController.Resolver);
}
else
serviceProvider = ControllerContext.Controller as IServiceProvider;
return serviceProvider;
}
//ripped from v3 RTM source
private IEnumerable<ModelValidationResult> ConvertResults(
IEnumerable<ValidationResult> results)
{
foreach (ValidationResult result in results)
{
if (result != ValidationResult.Success)
{
if (result.MemberNames == null || !result.MemberNames.Any())
{
yield return new ModelValidationResult { Message = result.ErrorMessage };
}
else
{
foreach (string memberName in result.MemberNames)
{
yield return new ModelValidationResult
{ Message = result.ErrorMessage, MemberName = memberName };
}
}
}
}
}
}
End Code
With that class in place, you can register this as the default adapter for IValidatableObject
instances with the line:
DataAnnotationsModelValidatorProvider.
RegisterDefaultValidatableObjectAdapterFactory(
(metadata, context) => new ValidatableObjectAdapterEx(metadata, context)
);