5

I have a ASP.NET WebApi project that I am working on. The boss would like the returns to support "partial response", meaning that though the data model might contain 50 fields, the client should be able to request specific fields for the response. The reason being that if they are implementing for example a list they simply don't need the overhead of all 50 fields, they might just want the First Name, Last Name and Id to generate the list. Thus far I have implemented a solution by using a custom Contract Resolver (DynamicContractResolver) such that when a request comes in I am peeking into it through a filter (FieldListFilter) in the OnActionExecuting method and determining if a field named "FieldList" is present and then if it is I am replacing the current ContractResolver with a new instance of my DynamicContractResolver and I pass the fieldlist to the constructor.

Some sample code

DynamicContractResolver.cs

protected override IList<JsonProperty> CreateProperties(Type type, Newtonsoft.Json.MemberSerialization memberSerialization)
    {
        List<String> fieldList = ConvertFieldStringToList();

        IList<JsonProperty> properties = base.CreateProperties(type, memberSerialization);
        if (fieldList.Count == 0)
        {
            return properties;
        }
        // If we have fields, check that FieldList is one of them.
        if (!fieldList.Contains("FieldList"))
            // If not then add it, FieldList must ALWAYS be a part of any non null field list.
            fieldList.Add("FieldList");
        if (!fieldList.Contains("Data"))
            fieldList.Add("Data");
        if (!fieldList.Contains("FilterText"))
            fieldList.Add("FilterText");
        if (!fieldList.Contains("PageNumber"))
            fieldList.Add("PageNumber");
        if (!fieldList.Contains("RecordsReturned"))
            fieldList.Add("RecordsReturned");
        if (!fieldList.Contains("RecordsFound"))
            fieldList.Add("RecordsFound");
        for (int ctr = properties.Count-1; ctr >= 0; ctr--)
        {
            foreach (string field in fieldList)
            {
                if (field.Trim() == properties[ctr].PropertyName)
                {
                    goto Found;
                }
            }
            System.Diagnostics.Debug.WriteLine("Remove Property at Index " + ctr + " Named: " + properties[ctr].PropertyName);
            properties.RemoveAt(ctr);
        // Exit point for the inner foreach.  Nothing to do here.
        Found: { }
        }
        return properties;
    }

FieldListFilter.cs

public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            throw new HttpResponseException(HttpStatusCode.BadRequest);
        }
        // We need to determine if there is a FieldList property of the model that is being used.
        // First get a reference to the model.
        var modelObject = actionContext.ActionArguments.FirstOrDefault().Value;
        string fieldList = string.Empty;
        try
        {
            // Using reflection, attempt to get the value of the FieldList property
            var fieldListTemp = modelObject.GetType().GetProperty("FieldList").GetValue(modelObject);
            // If it is null then use an empty string
            if (fieldListTemp != null)
            {
                fieldList = fieldListTemp.ToString();
            }
        }
        catch (Exception)
        {
            fieldList = string.Empty;
        }

        // Update the global ContractResolver with the fieldList value but for efficiency only do it if they are not the same as the current ContractResolver.
        if (((DynamicContractResolver)GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ContractResolver).FieldList != fieldList)
        {
            GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new DynamicContractResolver(fieldList);
        }
    }

I can then send a request with the json content payload looking as such:

{
  "FieldList":"NameFirst,NameLast,Id",
  "Data":[
    {
      "Id":1234
    },
    {
      "Id":1235
    }
  ]
}

and I will receive a response like so:

{
  "FieldList":"NameFirst,NameLast,Id",
  "Data":[
    {
      "NameFirst":"Brian",
      "NameLast":"Mueller",
      "Id":1234
    },
    {
      "NameFirst":"Brian",
      "NameLast":"Mueller",
      "Id":1235
    }
  ]
}

I believe that using the ContractResolver might run into threading issues. If I change it for one request is it going to be valid for all requests thereafter until someone changes it on another request (seems so through testing) If that is the case, then I don't see the usefulness for my purpose.

In summary, I am looking for a way to have dynamic data models such that the output from a request is configurable by the client on a request by request basis. Google implements this in their web api and they call it "partial response" and it works great. My implementation works, to a point but I fear that it will be broken for multiple simultaneous requests.

Suggestions? Tips?

4
  • Just FYI...check the $select feature support for Json formatter which is coming in next release: aspnetwebstack.codeplex.com/… Commented Jun 10, 2013 at 21:48
  • Don't think I have time to wait for the next version. We have a demo in less than 2 months and I've got a lot to implement. More to the point, is my method fundamentally broken. I had implemented based on suggestions and tutorials from around the web but I just have some questions about the foundation of it that I don't fully understand. Commented Jun 10, 2013 at 22:02
  • if you create a new instance of DynamicContractResolver on each request and use it as the ContractResolver, then there shouldn't be a concurrency issue. Commented Jun 11, 2013 at 0:11
  • muratgu, I think you answered my question, thank you! and Kiran - thanks for the info I have filed it away for later use. I will give this a day to see if anyone else has suggestions or comments and then I will close it. Commented Jun 11, 2013 at 2:37

3 Answers 3

6

A simpler solution that may work.

Create a model class with all 50 members with nullable types. Assign values to the requested members. Just return the result in the normal way.

In your WebApiConfig.Register() you must set the null value handling.

   config.Formatters.JsonFormatter.SerializerSettings =
        new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore };
Sign up to request clarification or add additional context in comments.

3 Comments

Now this sounds like an interesting alternative that I had not considered. I'll look into this right away as I agree that it would simplify some of the process and move the logic into the data translation layer of the API where I can have more control as well.
I implemented this solution and did away with my DynamicContractResolver. This seems to be working very well and has the added bonus of being more readable, more understandable and giving me some added flexibility to have such things as "required return fields" and such. Thanks for the assistance.
config.Formatters.JsonFormatter.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
1

You must not touch the configuration. You need the contract resolver on per-request basis. You can use it in your action method like this.

public class MyController : ApiController
{
    public HttpResponseMessage Get()
    {
        var formatter = new JsonMediaTypeFormatter();
        formatter.SerializerSettings.ContractResolver = 
              new DynamicContractResolver(new List<string>()
                       {"Id", "LastName"}); // you will get this from your filter

        var dto = new MyDto()
              { FirstName = "Captain", LastName = "Cool", Id = 8 };

        return new HttpResponseMessage()
        {
            Content = new ObjectContent<MyDto>(dto, formatter)
        };
        // What goes out is {"LastName":"Cool","Id":8}
    }
}

By doing this, you are locking yourself into JSON content type for response messages but you have already made that decision by using a Json.NET specific feature. Also, note you are creating a new JsonMediaTypeFormatter. So, anything you configure to the one in the configuration such as media type mapping is not going to be available with this approach though.

1 Comment

Badri, it's not true that by touching the global configuration I am locking myself into Json. I have already tested this by sending the request headers accept: application/xml and it returns xml regardless of the contractresolver because it's bypassed on the return path and it uses the xml resolver. I do not want to lock myself into json either, it's a requirement of the project that the end user be able to get back json or xml upon request. The boss is not concerned about partial content if they choose xml however so I am not concerned with that side of the process at this point.
0

I know this question is from many years ago, but if you're looking to do this with modern releases of the framework, I'd recommend nowadays to use OData services (http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/using-select-expand-and-value).

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.