Twitter Feed Popout byInfofru

OverrideThis.com

Adventures in .NET Software Craftsmanship!

MVC Model Validation–Refactor (Part Deux)

This post enhances the functionality of the code in the Blog titled “MVC2 Validation and Testing” at the following link. 
http://blog.overridethis.com/blog/post/2010/04/22/MVC2-Model-Validation-and-Testing-Scenarios.aspx

 

A comment was made on one of my previous blogs requesting additional functionality for the extension method I wrote in April for testing controller action’s that rely on MVC2 Model Validation.  The request was simple, to be able to test class level validation attributes such as the ones found on the ChangePasswordModel in the default MVC2 project template.   Well, as requested!

 

public static class ControllerExtensions {

    public static ActionResult CallWithModelValidation<TController, TResult, TModel>(
            this TController controller, 
            Func<TController, TResult> action, 
            TModel model)
        where TController : Controller
        where TResult : ActionResult
        where TModel : class {

        var provider = new DataAnnotationsModelValidatorProvider();

        var metadata = new List<ModelMetadata>();
        metadata.Add(GetMetadataForClass(model));
        metadata.AddRange(GetMetadataForProperties(model));

        foreach (ModelMetadata modelMetadata in metadata) {
            ApplyValidatorsFromMetadata(model, provider, modelMetadata, controller);
        }        
        
        return action(controller);
    }

    private static ModelMetadata GetMetadataForClass<TModel>(TModel model) 
        where TModel : class {

        return ModelMetadataProviders
            .Current
            .GetMetadataForType(() => model, typeof (TModel));
    }

    private static IEnumerable<ModelMetadata> GetMetadataForProperties<TModel>(TModel model)
       where TModel : class {
        return ModelMetadataProviders
            .Current
            .GetMetadataForProperties(model, typeof(TModel));
    }

    private static void ApplyValidatorsFromMetadata<TModel, TController>(TModel model, 
        DataAnnotationsModelValidatorProvider provider, 
        ModelMetadata modelMetadata, 
        TController controller)
            where TController : Controller
            where TModel : class {
        
        IEnumerable<ModelValidator> validators = provider
            .GetValidators(modelMetadata, new ControllerContext());
            
        foreach (ModelValidator validator in validators) {
            IEnumerable<ModelValidationResult> results = validator.Validate(model);
            foreach (ModelValidationResult result in results)
                controller.ModelState.AddModelError(
                    modelMetadata.PropertyName ?? "Model", 
                    result.Message);
        }
    }
}

MVC Extensibility–CMAP Code Camp

downloadThe following is the download link for the demo code and slides for my presentation on MVC Extensibility done at the Central Maryland Association of .NET Professionals Code Camp (CMAP Code Camp).  Thanks to all who attended my presentation, it was extremely fun plowing through a ton of code all across the stack of the ASP.NET MVC Framework request execution lifecycle.

 

Download Demo Code & Slides

 

Also, big thanks to Christopher Steen and Randy Hayes for hosting and allowing me to participate as a speaker in such a wonderful event.

Codes and Slides on MVC Extensibility

I have made my slides and code available from my presentation on MVC Extensibility at the MVC Conference.   Before I give out the details,  I would like to congratulate the organizers of the event for their great work on making this amazing event happen.  In particular, I would like to extend a personal thanks to Eric Hexter, and Javier Lozano.  Thanks again guys, for giving me the opportunity to speak on such an amazing digital venue.

 

The the slides and code are available at codeplex in this location.

http://mvcextensibility.codeplex.com

Presenting at MVCConf

I am excited to announce that I will be participating as a presenter at MVCConf.  MVC Conference (MVCConf) is a free virtual conference focused on one thing only, writing awesome application on top of the ASP.NET MVC applications, for more information go to http://www.mvcconf.com

 

I will be doing a presentation on MVC Extensiblity focused on plugging functionality onto your MVC application to enforce separation of concerns therefore enhancing testability. The following are the preliminary details:

 

MVC2 Extensibility

Learn the basic extensibility points of the ASP.NET MVC Framework by refactoring an existing code base and making changes that will make your application less fragile and easier to test.  The main focus will be how to leverage dependency injection, routes, action filters,  model binders, etc. to build a better all-around application.

 

Hope to see you online on the 22h of July!

MVC2 Validation and Testing – Refactored

This post is to fix a bug enhance the functionality in the original Blog Post titled “MVC2 Validation and Testing” at the following link. 
http://blog.overridethis.com/blog/post/2010/04/22/MVC2-Model-Validation-and-Testing-Scenarios.aspx


A while back I wrote about testing Controller Actions that rely on MVC2’s new Model Validation.  I had been using the technique described in the post for a while on most of my MVC2 applications without running into any significant issue until I tried to combine validators on a single property of my Model.  The following are the modified testing scenarios that would not work with the code I provided in my originally post.  Apparently, the code I wrote originally does not target all validators that are applied through Metadata.

 

namespace WebSite.Services {

    public interface ICommentService {
        void AddComment(string email, string message);
    }

}
namespace WebSite.Models {
    
    using System.ComponentModel.DataAnnotations;

    public class AddCommentRequest {
        private const string EMAIL_REGEX = 
           @"^(([\w-]+\.)+[\w-]+|([a-zA-Z]{1}|[\w-]{2,}))@"
            + @"((([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\."
            + @"([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])){1}|"
            + @"([a-zA-Z]+[\w-]+\.)+[a-zA-Z]{2,4})$";
        [Required, RegularExpression(EMAIL_REGEX)]
        public string Email { get; set; }
        [Required]
        public string Message { get; set; }
    }

    public class AddCommentResult {
        public bool Success { get; set; }
        public string Message { get; set; }
    }
}
namespace WebSite.Controllers {

    using System.Web.Mvc;
    using Models;
    using Services;

    public class CommentController : Controller {

        private readonly ICommentService service;

        public CommentController() {

        }

        public CommentController(ICommentService service) {
            this.service = service;
        }

        [HttpPost]
        public ActionResult AddComment(AddCommentRequest request) {
            if (ModelState.IsValid) {
                this.service.AddComment(request.Email, request.Message);
                return Json(new AddCommentResult {Success = true, Message = "Success"});
            }
            return Json(
                new AddCommentResult { 
                        Success = false, 
                        Message = "The 'message' is a required field." 
                });
        }
    }
}

 

The following Test validate the Model Validation functionality enforce and needless to say if we use the original ControllerExtension for validation we get less than ideal results.

 

[TestFixture]
public class CommentControllerTests {

    private string VALID_MESSAGE = "SOME MESSAGE!";
    private string VALID_EMAIL   = "someemail@yahoo.com";
    private string NOT_VALID_MESSAGE = string.Empty;
    private string NOT_VALID_EMAIL   = "someemail@@@@@@c";
    
    [Test]
    public void CommentControllerCanAddValidMessage() {
        Assert.IsTrue(this.HttpPostToAddComments(VALID_EMAIL, VALID_MESSAGE).Success);
    }

    [Test]
    public void CommentControllerCannotAddWithoutValidMessage() {
        Assert.IsTrue(this.HttpPostToAddComments(VALID_EMAIL, NOT_VALID_MESSAGE).Success);
    }    

    [Test]
    public void CommentControllerCannotAddWithoutRequiredEmail() {
        Assert.IsTrue(this.HttpPostToAddComments("", VALID_MESSAGE).Success);
    }    

    [Test]
    public void CommentControllerCannotAddWithourValidEmailAddress() {
        Assert.IsTrue(this.HttpPostToAddComments(NOT_VALID_EMAIL, VALID_MESSAGE).Success);
    }

    private AddCommentResult HttpPostToAddComments(string email, string message) {
        
        var mockOfICommentService = new Mock<ICommentService>();
        mockOfICommentService.Setup(m => m.AddComment(VALID_EMAIL, VALID_MESSAGE)).AtMostOnce();
        var model = new AddCommentRequest {Email = email, Message = message};
        var controller = new CommentController(mockOfICommentService.Object);
        var result = controller
		.CallWithModelValidation(c => c.AddComment(model), model);
        mockOfICommentService.Verify();
        return (AddCommentResult)((JsonResult) result).Data;
    }
}

 

This time around I feel like I did better research to find a suitable solution by looking at the ASP.NET MVC 2 source code and its tests.   In my original post, I had overlooked a couple of reusable tools already in place in the ASP.NET MVC source code that would have made my work a whole lot easier like the ModelMetadataProviders, and DataAnnotationsModelValidatorProvider.   Of course, there is always room for improvement, but the following is the refactored ControllerExtensions class that allows for all the previous tests to go green.

 

public static class ControllerExtensions {

    public static ActionResult CallWithModelValidation<C, R, T>(this C controller
           , Func<C, R> action
           , T model)
        where C : Controller
        where R : ActionResult
        where T : class {
        DataAnnotationsModelValidatorProvider provider = new DataAnnotationsModelValidatorProvider();
        IEnumerable<ModelMetadata> metadata = ModelMetadataProviders
			.Current
			.GetMetadataForProperties(model, typeof(T));
        foreach (ModelMetadata modelMetadata in metadata) {
            IEnumerable<ModelValidator> validators = provider
				.GetValidators(modelMetadata, new ControllerContext());
            foreach (ModelValidator validator in validators) {
                IEnumerable<ModelValidationResult> results = validator.Validate(model);
                foreach (ModelValidationResult result in results)
                    controller.ModelState.AddModelError(modelMetadata.PropertyName, result.Message);
            }
        }
        return action(controller);
    }
}