Search code examples
jsonasp.net-web-apiodata

large json argument in webapi odata


I got a WebApi OData service where I need an action to take an arbitrary json parameter.

Is there a way to have something like a JObject as an OData action parameter? action.Parameter<JObject>() does not work.

The complex type 'Newtonsoft.Json.Linq.JContainer' has a reference to itself through the property 'Parent'. A recursive loop of complex types is not allowed.

It works using a string argument but it means an unnecessary conversion in both ends for all requests. The json might also be large (100kb+) so I assume it puts pressure on the Large Object Heap to use strings.


Solution

  • OData services are normally strongly-typed, so you'll have to go out of your way to overcome Web API's built-in mapping from JSON to CLR types.

    First, define a new media type formatter that reads JTokens. Note the custom media type in use.

    public class RawJsonMediaTypeFormatter : MediaTypeFormatter
    {
        private static readonly MediaTypeHeaderValue _customMediaType = 
            MediaTypeHeaderValue.Parse("application/prs.adrianm+json");
    
        public RawJsonMediaTypeFormatter() : base()
        {
            this.Intialize();
        }
    
        protected RawJsonMediaTypeFormatter(MediaTypeFormatter formatter) : base(formatter)
        {
            this.Intialize();
        }
    
        protected void Intialize()
        {
            this.SupportedMediaTypes.Add(_customMediaType);
        }
    
        public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
        {
            if (type == typeof(JToken) && mediaType.MediaType == _customMediaType.MediaType)
            {
                return this;
            }
    
            return base.GetPerRequestFormatterInstance(type, request, mediaType);
        }
    
        public override bool CanReadType(Type type)
        {
            return type == typeof(JToken);
        }
    
        public override bool CanWriteType(Type type)
        {
            return false;
        }
    
        public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
        {
            return this.ReadFromStreamAsync(type, readStream, content, formatterLogger, default(CancellationToken));
        }
    
        public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger, CancellationToken cancellationToken)
        {
            object result;
    
            using (var reader = new JsonTextReader(new StreamReader(readStream)))
            {
                result = JToken.ReadFrom(reader);
            }
    
            return Task.FromResult(result);
        }
    }
    

    As part of Web API registration, add an instance of the custom formatter to the HttpConfiguration and declare your OData action. Note that the type of the action parameter is vanilla object.

            config.Formatters.Clear();
            config.Formatters.AddRange(ODataMediaTypeFormatters.Create());
            config.Formatters.Add(new RawJsonMediaTypeFormatter());
    
            var builder = new ODataConventionModelBuilder();
            var arbitraryJsonAction = builder.Action("ArbitraryJson");
            arbitraryJsonAction.Parameter<object>("json");
            arbitraryJsonAction.Returns<string>();
    

    Add a controller method for the action.

        [HttpPost]
        [ODataRoute("ArbitraryJson")]
        public IHttpActionResult ArbitraryJson(JToken json)
        {
            return this.Ok(json.ToString());
        }
    

    On the client, remember to set Content-Type to the custom media type handled by the custom formatter:

    POST http://host/ArbitraryJson
    Content-Type: application/prs.adrianm+json
    
    [1, 2, {"foo": true }]
    

    The response payload should look something like the following:

    {
      "@odata.context": "http://host/$metadata#Edm.String",
      "value": "[\r\n  1,\r\n  2,\r\n  {\r\n    \"foo\": true\r\n  }\r\n]"
    }