14

We have quite a few validation methods that need to access repositories / database to do their work. So far we have been using the service locator pattern (albeit sparingly) to accomplish this in custom ValidationAttributes:

public override bool IsValid(object value)
{
    // use custom service locator in our app's infrastructure
    var repos = DependencyInjector.Current.GetService<IXyzRepository>();
    ...
}

I know this is :( upon as an anti-pattern, and we would like to use a more correct approach. We use unity, and I read this post that says to use a build-up method. However the link in the accepted answer says that the documentation is outdated (retired content).

The solution does not need to use a validation attribute, I suppose it could use IValidatableObject, however the problem remains: how to inject the dependency into the model. Do we need a custom model binder to do this?

Another solution would be to perform the validation in the controller, where dependency injection is easy. To me this feels cluttered though. I would like the model to be validated by the time it gets to the action method.

Also we sometimes use [RemoteAttribute] to perform some of these validations on the client. Currently those methods construct a viewmodel and delegate validation to the model by using the static Validator.TryValidateObject method.

How have you accomplished validation that requires an injected dependency to do its work, without using the SL anti-pattern?

Community
  • 1
  • 1
danludwig
  • 46,965
  • 25
  • 159
  • 237
  • 1
    Service Locator is not an anti-pattern imho. It's an abused pattern that is an excellent solution to problems where constructor injection is not possible. – jgauffin Jan 24 '12 at 11:30

2 Answers2

12

How have you accomplished validation that requires an injected dependency to do its work, without using the SL anti-pattern?

I use FluentValidation.NET to perform validation in my applications. It allows me to inject dependencies into my validators. It has a really nice integration with ASP.NET MVC. It also supports automatic client side validation for the standard rules just the same way as data annotations using jquery unobtrusive validate:

  • NotNull/NotEmpty
  • Matches (regex)
  • InclusiveBetween (range)
  • CreditCard
  • Email
  • EqualTo (cross-property equality comparison)
  • Length

I have never used data annotations to perform validation. They are absolutely useless when you need to handle some more complex validation scenarios where you need to validate dependent properties and even use some service. I put complex in italics in the previous sentence because, I don't think that validating that one of the 2 properties is required is a really complex validation scenario and yet, just checkout the amount of infrastructure crap you have to write in order to implement it using data annotations. Looking at this code you no longer know what you are validating.

Community
  • 1
  • 1
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • Looking at the docs, I can't find any info on how you might go about validating on the client using this lib. Do you use this for validation on the client? If so, can you update your answer with links? – danludwig Jan 19 '12 at 16:50
  • 1
    @olivehour, you could use the unobtrusive jquery validation exactly the same way as you currently do with data annotations. When you use FV.NET you register a custom model metadata provider which is used by HTML helpers such as TextBoxFor to emit HTML5 data-* attributes on input fields. It works with the standard rules. Here's the list of standard rules that are automatically supported for client side validation: http://fluentvalidation.codeplex.com/wikipage?title=mvc&referringTitle=Documentation. I have updated my answer to include a link to the documentation showing the integration with MVC. – Darin Dimitrov Jan 19 '12 at 16:56
  • Awesome. One more point of clarity, regarding the original question: When you need to perform validation on the client that needs DI, do you use RemoteAttribute? Are you able to keep validation in one place, so that the same rules can be applied for both server-side and client-side validation? – danludwig Jan 19 '12 at 17:21
  • @olivehour, yes with remote validation you could still use the Remote attribute on your model and then have a controller action which will be invoked through AJAX to perform the validation. Obviously all that this controller action will do is to query the service layer (that was already injected into your controller) to perform the validation. The same will be done in the FV's server side validator => a query to the service layer method that was injected into it. But the whole business validation rule will obviously be inside the service layer method. – Darin Dimitrov Jan 19 '12 at 17:30
1

Inject your validation into your Model.

Validation Attributes can become awkward to work with when your validation stories become more complex. Yuck!

I like to use Entity Framework with Code First. I have full control of my model at that point. I also use FluentValidation like @Darin Dimitrov and I really like its ease of use and simple syntax.

Here’s how you put it together. I assume you have assembly with your interfaces or contracts.

This will be the base interface for your models…

using System.ComponentModel;
using FluentValidation.Results;

public interface IAbstractBase : IDataErrorInfo
{
    bool IsValid { get; }
    ValidationResult SelfValidate();
}

and its counterpart in your business layer looks like this…

using System;
using System.Linq;
using FluentValidation.Results;
using Contracts;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

public abstract class AbstractBase : IAbstractBase
{
    #region IDataErrorInfo

    public abstract ValidationResult SelfValidate();

    [NotMapped]
    public bool IsValid
    {
        get
        {
            return SelfValidate().IsValid;
        }
    }

    [NotMapped]
    public string Error
    {
        get
        {
            var results = SelfValidate().Errors.Select(s => string.Format("● {0}{1}", s.ErrorMessage, Environment.NewLine)).ToArray();
            return string.Join("", results);
        }
    }

    [NotMapped]
    public IList<ValidationFailure> Errors
    {
        get
        {
            var results = SelfValidate().Errors;
            return results;
        }
    }

    [NotMapped]
    public string this[string columnName]
    {
        get
        {
            var validationResults = SelfValidate();
            if (validationResults == null) return string.Empty;
            var columnResults = validationResults.Errors.FirstOrDefault(x => string.Compare(x.PropertyName, columnName, true) == 0);
            return columnResults != null ? columnResults.ErrorMessage : string.Empty;
        }
    }
    #endregion
}

This is your base class for your models. Make sure you implement the abstract method in your models. It should look like this.

public class MyModel : AbstractBase, IMyModel
{
    private AbstractValidator<IMyModelValidator> _myModelValidator;
    public MyModel():this(new MyModelValidator()){};
    public MyModel(AbstractValidator<IMyModelValidator> myModelValidator){
         _myModelValidator = myModelValidator;
    };

    public int MyModelId { get; set; }
    public string Name { get; set; }
    public DateTime CreatedDate { get; set; }

    public override ValidationResult SelfValidate()
    {
        return _myModelValidator.Validate(this);
    }
}

Your validator class will look something like this.

 public class MyModelValidator : AbstractValidator<IMyModelValidator>
    {
        private IMyModelProvider _myModelProvider;
        public MyModelValidator(IMyModelProvider myModelProvider){ _myModelProvider = myModelProvider;};
        private void SetRules()
        {
            RuleFor(x => x.Name).NotEmpty().WithMessage("Please specify a project name.");
            RuleFor(x => x.Name.Length).LessThanOrEqualTo(100).WithMessage("The project name must be less than or equal to 100 characters.");
        }

        public override ValidationResult Validate(IMyModel instance)
        {
            SetRules();
            return base.Validate(instance);
        }
    }

Pass your validation results from your Model to your view in your Controller using the following call in your controller.

 TryValidateModel(your model here);

After you call this in your controller call your model.IsValid property.

Make sure you register everything and you should be good to go. I assume you can fill in the missing pieces.

The big picture looks like this: enter image description here

  • Thanks. I know EF 4.1 CF automatically validates using the dataannotations validators during SaveChanges. I'm thinking of posting another question regarding whether or not FV.NET can be plugged in there as well. Do you know if EF 4.1 has a similar validation provider hook? – danludwig Jan 20 '12 at 20:44
  • @olivehour - It sure can. Remember in EF 4.1 you have more control. So, in your EF 4.1 provider - on lets say a update method - you would call the IsValid property in a if statement before you SaveChanges. Works great. If your passing in strong types to your Controller you can build it up using a binder before it gets to the controller. Check if it's valid from there and pass it along to your persistance layer if it passes your IsValid condition. –  Jan 20 '12 at 21:00
  • It sounds like you're suggesting the repository impl takes a dependency on FV.NET. I guess was thinking more in terms of something that you can add as a convention to the DbModelBuilder. – danludwig Jan 20 '12 at 21:15
  • Actually the repository doesn't have a dependency on FV.NET. Remember the MyModel object graph - if it is built-up - will already have the validator attached by the time it gets to the persistance layer. –  Jan 20 '12 at 21:30
  • I will look into this too. Seems like a lot of work, but will give it a shot. If I have any questions, would you mind moving this discussion to chat later? – danludwig Jan 20 '12 at 22:18
  • I'll post an example application for your review. –  Jan 20 '12 at 23:28
  • @olivehour Feel free to chat any time. –  Jan 22 '12 at 01:58