다음을 통해 공유


How to bind to custom objects in action signatures in MVC/WebAPI

MVC provides several ways for binding your own arbitrary parameter types.  I’ll describe some common MVC ways and then show how this applies to WebAPI too. You can view this as a MVC-to-WebAPI migration guide.  (Related reading: How WebAPI binds parameters )

Say we have a complex type, Location, which just has an X and Y. And we want to create that by invoking a Parse(string) function.  The question then becomes: how do I wire up my custom Parse(string) function into WebAPI’s parameter binding system?

Query string: /?loc=123,456  

And then this action gets invoked and the parameter is bound from the query string:

         public object MyAction(Location loc) 
        {
            // expect that loc.X = 123, loc.Y = 456
        }

 

Here’s the C# code for the my Location class, plus the essential parse function:

     // A complex type
    public class Location
    {        
        public int X { get; set; }
        public int Y { get; set; }

        // Parse a string into a Location object. "1,2" --> Loc(X=1,Y=2)
        public static Location TryParse(string input)
        {
            var parts = input.Split(',');
            if (parts.Length != 2)
            {
                return null;
            }

            int x,y;
            if (int.TryParse(parts[0], out x) && int.TryParse(parts[1], out y))
            {
                return new Location { X = x, Y = y };                
            }

            return null;
        }

        public override string ToString()
        {
            return string.Format("{0},{1}", X, Y);
        }
    }

 

Option Fail: what if I do nothing?

If you just define a Location class, but don’t tell WebAPI/MVC about the parse function, it won’t know how to bind it. It may make a best effort, but the Location parameter will be empty.

In WebAPI, we’ll see Location is a complex type, assume it’s coming from the request’s body and so try to invoke a Formatter on it.  WebAPI will search for a formatter that matches the content type and claims to handle the Location type. The formatter likely won’t find anything in the body and leave the parameter empty.

 

Option #1: Manually call the parse function

You can always take the string in the action signature and manually call the parse function yourself.

         public object MyAction1(string loc)
        {
            Location loc2 = Location.TryParse(loc); // explicitly convert string
            // now use loc2 ... 
        }

You can still do this in WebAPI, exactly as is.

What does WebAPI do under the hood? In WebAPI, the string parameter is seen as a simple type, and so it uses model binding to pull ‘loc’ from the query string.

 

Option #2: Use a TypeConverter to make the complex type be simple

Or we can do it with a TypeConverter. This just teachers the model binding system about where to find the Parse() function for the given type.

     public class LocationTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            if (sourceType == typeof(string))
            {
                return true;
            }
            return base.CanConvertFrom(context, sourceType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, 
           System.Globalization.CultureInfo culture, object value)
        {
            if (value is string)
            {
                return Location.TryParse((string) value);
            }

            return base.ConvertFrom(context, culture, value);
        }
    }

And then add the appropriate attribute to the Location’s type declaration:

    [TypeConverter(typeof(LocationTypeConverter))]
   public class Location
   {  ... }

Now in both MVC and WebAPI, your action will get called and the Location parameter is bound:

 

 public object MyAction(Location loc)        
{
   // use loc
}

What does WebAPI do under the hood? The presence of a TypeDescriptor that converts from string means that WebAPI classifies this a “simple type”. Simple types use model binding. WebAPI will get ‘loc’ from the query string by matching the parameter name, see the parameter’s type is “Location” and then invoke the TypeConverter to convert from string to Location.

 

Option #3: Use a custom model binder

Another way is to use a custom model binder. This essentially just teachers the model binding system about the Location parse function. There are two key parts here:

  a) defining the model binder and

  b) wiring it up to the system so that it gets used.

Part a) Writing a custom model binder:

Here’s in MVC:

     public class LocationModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            string key = bindingContext.ModelName;
            ValueProviderResult val = bindingContext.ValueProvider.GetValue(key);
            if (val != null)
            {
                string s = val.AttemptedValue as string;
                if (s != null)
                {
                    return Location.TryParse(s);
                }
            }
            return null;
        }
    }

Of course, once you’ve written a custom model binder, you can do a lot more with it than just call a Parse() function. But that’s another topic…

Defining a custom model binder is very similar in WebAPI. We still have a correpsonding IModelBinder interface, and the design pattern is the same, but its signature is slightly different:

     public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)

MVC takes in a controller context, whereas WebAPI takes in an actionContext (which has a reference to a controller context). And MVC returns the object for the model, whereas WebAPI  returns a bool and sets the model result on the binding context. (As a reminder, WebAPI and MVC share design patterns, but have different types. So while you can often cut and paste code between them, you may need to touch up namespaces)

 

Part B) now we need to wire up the model binder.

In both MVC and WebAPI, there are 3 places you can do this.

1) The highest precedence location is the one closest to the parameter. Just add a [ModelBinder] attribute on the parameter’s signature

         public object  MyAction2(
             [ModelBinder(typeof(LocationModelBinder))] 
            Location loc) // Use model binding to convert
        {
            // use loc...
        }

This is the same as WebAPI. (In WebAPI, this was only supported after beta, so if you’re pre-RTM, you’ll need the latest sources)

2) add a [ModelBinder] attribute on the type’s declaration. 

          [ModelBinder(typeof(LocationModelBinder))]        
 public class Location { ... }

Same as WebAPI, like #1.

3) Change it in a global config setting

In MVC, this is in the global.asax file. An easy way is just like so:

        ModelBinders.Binders.Add(typeof(Location), new LocationModelBinder());            

In WebAPI, registration is on the HttpConfiguration object. Web API strictly goes through the service resolver. WebAPI does have a gotcha that you need to register custom model binders at the front because the default list has MutableObjectModelBinder which zealously claims all types and so would shadow your custom binder if it were just appended to the end.

             
            config.Services.Insert(typeof(System.Web.Http.ModelBinding.ModelBinderProvider), 
                  0, // Insert at front to ensure other catch-all binders don’t claim it first
                  new LocationModelBinderProvider()); 

And then in WebAPI, you still need to add an empty [ModelBinder] attribute on the parameter to tell WebAPI to look in the model binders instead of trying to use  a formatter on it.

The [ModelBinder] doesn’t need to specify the binder type because you provided it in the config object.

         public object  MyAction2([ModelBinder] Location loc) // Use model binding to convert
        {
            // use loc...
        }

What does WebAPI do under the hood? In all 3 cases, WebAPI sees a [ModelBinder] attribute associated with the parameter (either on the Parameter or on the Parameter’s Type’s declaration). The model binder attribute can either supply the binder directly (as in cases #1 and #2) or fetch the binder from the config (case #3). WebAPI then invokes that binder to get a value for the parameter.

 

         

Other places to hook?

WebAPI is very extensible and you could try to hook other places too, but the ones above are the most common and easiest for this scenario. But for completeness sake, I’ll mention a few other options, which I may blog about more later:

  • For example, you could hook the IActionValueBinder (here’s an example of an MVC-style parameter binder), IHttpActionInvoker (to populate right before invoking the action), or even populate parameters through a filter.
  • By default, complex types try to come from the body, and the body is read via Formatters. So you could also try to provide a custom formatter. However, that’s not ideal because in our example, we wanted data from the query string and Formatters can’t read the query string.

Comments

  • Anonymous
    April 20, 2012
    If you're going to use the TryParse pattern, it should return a bool and use an out parameter; otherwise it is fairly confusing. Location.Parse would be the appropriate signature for this method.

  • Anonymous
    July 10, 2012
    The TryParse pattern is, as far as I've ever seen, only associated with value types. I believe the author simply misnamed the method. It should be Parse(), instead of TryParse().

  • Anonymous
    July 12, 2012
    @John - TryParse can be used with reference types too. Try parse is about converting a string back into a rich object; it doesn't really matter if that obejct is reference or value type. For example, see System.Net.IPAddress.TryParse.  IPAddress is a reference type.

  • Anonymous
    May 02, 2018
    Best content on model binding. Thanks.