Use custom ASP.NET MVC IValueProvider, without setting it globally?
Asked Answered
A

3

14

I want to be able to grab keys/values from a cookie and use that to bind a model.

Rather than building a custom ModelBinder, I believe that the DefaultModelBinder works well out of the box, and the best way to choose where the values come from would be to set the IValueProvider that it uses.

To do this I don't want to create a custom ValueProviderFactory and bind it globally, because I only want this ValueProvider to be used in a specific action method.

I've built an attribute that does this:

/// <summary>
/// Replaces the current value provider with the specified value provider
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class SetValueProviderAttribute : ActionFilterAttribute
{
    public SetValueProviderAttribute(Type valueProviderType)
    {
        if (valueProviderType.GetInterface(typeof(IValueProvider).Name) == null)
            throw new ArgumentException("Type " + valueProviderType + " must implement interface IValueProvider.", "valueProviderType");

        _ValueProviderType = valueProviderType;
    }

    private Type _ValueProviderType;

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        IValueProvider valueProviderToAdd = GetValueProviderToAdd();

        filterContext.Controller.ValueProvider = valueProviderToAdd;
    }

    private IValueProvider GetValueProviderToAdd()
    {
        return (IValueProvider)Activator.CreateInstance(_ValueProviderType);
    }
}

Unfortunately, the ModelBinder and its IValueProvider are set BEFORE OnActionExecuting (why?????). Has anyone else figured out a way to inject a custom IValueProvider into the DefaultModelBinder without using the ValueProviderFactory?

Alisiaalison answered 29/9, 2011 at 16:8 Comment(0)
A
3

Figured out how to do this. First, create a custom model binder that takes a value provider type in the constructor - but inherits from default modelbinder. This allows you to use standard model binding with a custom value provider:

/// <summary>
/// Uses default model binding, but sets the value provider it uses
/// </summary>
public class SetValueProviderDefaultModelBinder : DefaultModelBinder
{
    private Type _ValueProviderType;

    public SetValueProviderDefaultModelBinder(Type valueProviderType)
    {
        if (valueProviderType.GetInterface(typeof(IValueProvider).Name) == null)
            throw new ArgumentException("Type " + valueProviderType + " must implement interface IValueProvider.", "valueProviderType");

        _ValueProviderType = valueProviderType;
    }

    /// <summary>
    /// Before binding the model, set the IValueProvider it uses
    /// </summary>
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        bindingContext.ValueProvider = GetValueProvider();

        return base.BindModel(controllerContext, bindingContext);
    }

    private IValueProvider GetValueProvider()
    {
        return (IValueProvider)Activator.CreateInstance(_ValueProviderType);
    }
}

Then we create a model binding attribute that will inject the value provider type in the custom model binder created above, and use that as the model binder:

/// <summary>
/// On the default model binder, replaces the current value provider with the specified value provider.  Cannot use custom model binder with this.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
public class SetValueProviderAttribute : CustomModelBinderAttribute
{
    // Originally, this was an action filter, that OnActionExecuting, set the controller's IValueProvider, expecting it to be picked up by the default model binder
    // when binding the model.  Unfortunately, OnActionExecuting occurs AFTER the IValueProvider is set on the DefaultModelBinder.  The only way around this is
    // to create a custom model binder that inherits from DefaultModelBinder, and in its BindModel method set the ValueProvider and then do the standard model binding.

    public SetValueProviderAttribute(Type valueProviderType)
    {
        if (valueProviderType.GetInterface(typeof(IValueProvider).Name) == null)
            throw new ArgumentException("Type " + valueProviderType + " must implement interface IValueProvider.", "valueProviderType");

        _ValueProviderType = valueProviderType;
    }

    private Type _ValueProviderType;

    public override IModelBinder GetBinder()
    {
        var modelBinder = new SetValueProviderDefaultModelBinder(_ValueProviderType);
        return modelBinder;
    }
}
Alisiaalison answered 28/12, 2011 at 18:18 Comment(0)
T
10

You should still use a ValueProviderFactory in this case.

The method that you have to implement on your ValueProviderFactory has this signature:

IValueProvider GetValueProvider(ControllerContext controllerContext)

Within your implementation of that method you can inspect the controller context, and if the incoming request is for the controller/action that you want to leverage cookies on, return some CustomCookieValueProvider.

If you don't want to leverage cookies for the request, just return null and the framework will filter that out of from the list of Value Providers.

As a bonus, you might not want to hard code the logic for when to use the CustomCookieValueProvider into the ValueProviderFactory. You could, perhaps, leverage DataTokens to match when to use cookies with given routes. So add a route like this:

routes.MapRoute("SomeRoute","{controller}/{action}").DataTokens.Add("UseCookies", true);

Notice the DataTokens.Add() call in there, now inside you GetValueProvider method you could do something like this:

if (controllerContext.RouteData.DataTokens.ContainsKey("UseCookies"))
{
    return new CustomCookieValueProvider(controllerContext.RequestContext.HttpContext.Request.Cookies);
}

return null;
Thorr answered 7/10, 2011 at 14:59 Comment(1)
I get why @Oved D would want that as an Attribute. But the DataTokens solution is pretty elegant.Vanna
S
4

Here is an alternative that lets you specify IValueProviders as attributes against an actions parameters. This makes the IValueProviders transient and not Global.

public interface IControllerContextAware
{
    ControllerContext ControllerContext { get; set; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
public class ValueProviderAttribute : CustomModelBinderAttribute
{
    public Type[] ValueProviders { get; private set; }

    public ValueProviderAttribute(params Type[] valueProviders)
    {
        if (valueProviders == null)
        {
            throw new ArgumentNullException("valueProviders");
        }
        foreach (var valueProvider in valueProviders.Where(valueProvider => !typeof(IValueProvider).IsAssignableFrom(valueProvider)))
        {
            throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "The valueProvider {0} must be of type {1}", valueProvider.FullName, typeof(IValueProvider)), "valueProviders");
        }

        ValueProviders = valueProviders;
    }

    public override IModelBinder GetBinder()
    {
        return new ValueProviderModelBinder
            {
                ValueProviderTypes = ValueProviders.ToList(),
                CreateValueProvider = OnCreateValueProvider
            };
    }

    protected virtual IValueProvider OnCreateValueProvider(Type valueProviderType, ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var valueProvider = (IValueProvider)Activator.CreateInstance(valueProviderType);
        if (valueProvider is IControllerContextAware)
        {
            (valueProvider as IControllerContextAware).ControllerContext = controllerContext;
        }
        return valueProvider;
    }

    private class ValueProviderModelBinder : DefaultModelBinder
    {
        public IList<Type> ValueProviderTypes { get; set; }
        public Func<Type, ControllerContext, ModelBindingContext, IValueProvider> CreateValueProvider { get; set; }

        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var valueProviders = from type in ValueProviderTypes
                                 select CreateValueProvider(type, controllerContext, bindingContext);

            bindingContext.ValueProvider = new ValueProviderCollection(valueProviders.Concat((Collection<IValueProvider>)bindingContext.ValueProvider).ToList());

            return base.BindModel(controllerContext, bindingContext);
        }
    }
}

This is basically the code form the ModelBinderAttribute, but with a few tweaks. It isn't sealed and so you can alter the way in which the IValueProviders are created if need be.

Here is a simple example which looks in another field, possibly a hidden or encrypted field, and takes the data and puts it into another property.

Here is the model, which has no knowledge of the IValueProvider, but does know about the hidden field.

public class SomeModel
{
    [Required]
    public string MyString { get; set; }

    [Required]
    public string MyOtherString { get; set; }

    [Required]
    public string Data { get; set; }
}

THen we have the IValueProvider, in this case, my provider knows explicitly about my model, but this doesn't have to be the case.

public class MyValueProvider : IValueProvider, IControllerContextAware
{
    public ControllerContext ControllerContext { get; set; }

    public bool ContainsPrefix(string prefix)
    {
        var containsPrefix = prefix == "MyString" && ControllerContext.HttpContext.Request.Params.AllKeys.Any(key => key == "Data");
        return containsPrefix;
    }

    public ValueProviderResult GetValue(string key)
    {
        if (key == "MyString")
        {
            var data = ControllerContext.RequestContext.HttpContext.Request.Params["Data"];

            var myString = data.Split(':')[1];

            return new ValueProviderResult(myString, myString, CultureInfo.CurrentCulture);
        }
        return null;
    }
}

and then the action that ties all this together:

    [HttpGet]
    public ActionResult Test()
    {
        return View(new SomeModel());
    }

    [HttpPost]
    public ActionResult Test([ValueProvider(typeof(MyValueProvider))]SomeModel model)
    {
        return View(model);
    }
Striction answered 3/3, 2013 at 18:6 Comment(0)
A
3

Figured out how to do this. First, create a custom model binder that takes a value provider type in the constructor - but inherits from default modelbinder. This allows you to use standard model binding with a custom value provider:

/// <summary>
/// Uses default model binding, but sets the value provider it uses
/// </summary>
public class SetValueProviderDefaultModelBinder : DefaultModelBinder
{
    private Type _ValueProviderType;

    public SetValueProviderDefaultModelBinder(Type valueProviderType)
    {
        if (valueProviderType.GetInterface(typeof(IValueProvider).Name) == null)
            throw new ArgumentException("Type " + valueProviderType + " must implement interface IValueProvider.", "valueProviderType");

        _ValueProviderType = valueProviderType;
    }

    /// <summary>
    /// Before binding the model, set the IValueProvider it uses
    /// </summary>
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        bindingContext.ValueProvider = GetValueProvider();

        return base.BindModel(controllerContext, bindingContext);
    }

    private IValueProvider GetValueProvider()
    {
        return (IValueProvider)Activator.CreateInstance(_ValueProviderType);
    }
}

Then we create a model binding attribute that will inject the value provider type in the custom model binder created above, and use that as the model binder:

/// <summary>
/// On the default model binder, replaces the current value provider with the specified value provider.  Cannot use custom model binder with this.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
public class SetValueProviderAttribute : CustomModelBinderAttribute
{
    // Originally, this was an action filter, that OnActionExecuting, set the controller's IValueProvider, expecting it to be picked up by the default model binder
    // when binding the model.  Unfortunately, OnActionExecuting occurs AFTER the IValueProvider is set on the DefaultModelBinder.  The only way around this is
    // to create a custom model binder that inherits from DefaultModelBinder, and in its BindModel method set the ValueProvider and then do the standard model binding.

    public SetValueProviderAttribute(Type valueProviderType)
    {
        if (valueProviderType.GetInterface(typeof(IValueProvider).Name) == null)
            throw new ArgumentException("Type " + valueProviderType + " must implement interface IValueProvider.", "valueProviderType");

        _ValueProviderType = valueProviderType;
    }

    private Type _ValueProviderType;

    public override IModelBinder GetBinder()
    {
        var modelBinder = new SetValueProviderDefaultModelBinder(_ValueProviderType);
        return modelBinder;
    }
}
Alisiaalison answered 28/12, 2011 at 18:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.