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)
);