asp.net-mvcdependency-injectionninjectmodelbinders

Injecting Class into IModelBinder using Ninject


I have the following Custom Model Binder:

public class AllowAndSanitizeHtmlBinder : IModelBinder
{
    // initialize HtmlSanitizer (I want this to be injected)
    private HtmlSanitizer _htmlSanitizer = new HtmlSanitizer();
    sanitizer.PostProcessNode += (s, e) =>  
        (e.Node as IHtmlAnchorElement)?.SetAttribute("rel", "nofollow");

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var request = controllerContext.HttpContext.Request;
        var name = bindingContext.ModelName;

        // get the unvalidated user input
        var unsanitizedMessageHtmlString = request.Unvalidated[name]; 

        // removed script or any XSS threat from user input
        return _htmlSanitizer.Sanitize(unsanitizedMessageHtmlString); 
    }
}

The problem I have with this code is that I am doing all the initialization of HtmlSanitizer in the model binder class (this is violating SRP). Is it possible to inject the HtmlSanitizer into the above binder? I am using Ninject.

I have seen this question: the accepted answer suggests that a model binder should not depend on any service, I am not sure this is the case here... I think DI would simplify my code.


Solution

  • You need a custom IModelBinderProvider to achieve this.

    public class AllowAndSanitizeHtmlBinderProvider : IModelBinderProvider
    {
            public HtmlSanitizer Sanitizer{get;}
    
            public AllowAndSanitizeHtmlBinderProvider(HtmlSanitizer sanitizer)
            {
              Sanitizer = sanitizer;
            }
    
            public IModelBinder GetBinder(Type modelType)
            {
                if(modelType==typeof(string)) // I assume it's string
                    return new AllowAndSanitizeHtmlBinder (Sanitizer);
                return null;
            }
    }
    
    public class AllowAndSanitizeHtmlBinder : IModelBinder
    {
        private HtmlSanitizer _htmlSanitizer;
    
        public AllowAndSanitizeHtmlBinder(HtmlSanitizer sanitizer)
        {
          _htmlSanitizer = sanitizer;
        }
    
    
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var request = controllerContext.HttpContext.Request;
            var name = bindingContext.ModelName;
    
            // get the unvalidated user input
            var unsanitizedMessageHtmlString = request.Unvalidated[name]; 
    
            // removed script or any XSS threat from user input
            return _htmlSanitizer.Sanitize(unsanitizedMessageHtmlString); 
        }
    }
    

    then in you Ninject configuration

    kernel.Bind<IModelBinderProvider>().To<AllowAndSanitizeHtmlBinderProvider >();
    kernel.Bind<HtmlSanitizer>().ToMethod(ctx => { 
        var sanitizer = new HtmlSanitizer();
        sanitizer.PostProcessNode += (s, e) =>  
            (e.Node as IHtmlAnchorElement)?.SetAttribute("rel", "nofollow");
        return sanitizer;
    });
    

    An even better approach would be to define a factory for AllowAndSanitizeHtmlBinder which would hold the dependency on HtmlSanitizer. The Provider would then just receive the factory as dependency. This would mask the dependency on HtmlSanitizer to the provider.

    Also, it would allow to hide HtmlSanitizer behind an interface, and have all your non Ninject code address this interface. This would allow to hide this technical dependency from the other parts of your code.