asp.net-web-apiperformance-testingants

ModelBinder Request.Content.ReadAsStringAsync performance


I have a custom ModelBinder, which, when I load test my app, and run Ants profiler on, identified reading the Request.Content as string as a hotspot:

 public class QueryModelBinder : IModelBinder
    {
        public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
            var body = actionContext.Request.Content.ReadAsStringAsync().Result;

Is there a more efficient way of doing this? Or am I reading the ANTS profiler incorrectly?

enter image description here


Solution

  • How big is the content? Note that you might be seeing a lot of time because you are calling this network call in sync rather than async.

    You can potentially read the string earlier async and stash it in the request property.

    Alternatively you can write a formatter instead, and then decorate your parameter with [FromBody].

    The recommended approach here is to use a FromBody and a formatter, since it naturally fits with the WebAPI architecture:

    For that you would write a media type formatter:

    public class StringFormatter : MediaTypeFormatter
    {
        public StringFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/mystring"));
        }
    
        public override bool CanReadType(Type type)
        {
            return (type == typeof (string));
        }
    
        public override bool CanWriteType(Type type)
        {
            return false;
        }
    
        public override async Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger,
            CancellationToken cancellationToken)
        {
            if (!CanReadType(type))
            {
                throw new InvalidOperationException();
            }
    
            return await content.ReadAsStringAsync();
        }
    }
    

    Register it in webapiconfig.cs

    config.Formatters.Add(new StringFormatter());
    

    And consume in an action

    public string Get([FromBody]string myString)
    {
        return myString;
    }
    

    The other design (not as recommended because of coupling between the filter and the binder):

    Implement a model binder (this is super Naive):

    public class MyStringModelBinder : IModelBinder
    {
        public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
            // this is a Naive comparison of media type
            if (actionContext.Request.Content.Headers.ContentType.MediaType == "application/mystring")
            {
                bindingContext.Model = actionContext.Request.Properties["MyString"] as string;
                return true;
            }
    
            return false;
        }
    }
    

    Add an authroization filter (they run ahead of modelbinding), to you can async access the action. This also works on a delegating handler:

    public class MyStringFilter : AuthorizationFilterAttribute
    {
        public override async Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            if (actionContext.Request.Content.Headers.ContentType.MediaType == "application/mystring")
            {
                var myString = await actionContext.Request.Content.ReadAsStringAsync();
                actionContext.Request.Properties.Add("MyString", myString);
            }
        }
    }
    

    Register it in WebApi.Config or apply it to the controller:

    WebApiConfig.cs

    config.Filters.Add(new MyStringFilter());
    

    ValuesController.cs

    [MyStringFilter] // this is optional, you can register it globally as well
    public class ValuesController : ApiController
    {
        // specifying the type here is optional, but I'm using it because it avoids having to specify the prefix
        public string Get([ModelBinder(typeof(MyStringModelBinder))]string myString = null)
        {
            return myString;
        }
    }
    

    (Thanks for @Kiran Challa for looking over my shoulder, and suggesting the Authorization filter)

    EDIT: One thing to always remember with relatively large strings (consuming more than 85KB so about 40K Chars) can go into the Large Object heap, which will wreak havoc on your site performance. If you thing this is common enough, break the input down into something like a string builder/array of strings or something similar without contiguous memory. See Why Large Object Heap and why do we care?