Search code examples
jsonodataasp.net-web-apimediatypeformatter

Web API seems to be caching JsonFormatter with OData requests?


TL;DR: My OData requests seem to be hitting my custom JsonFormatter once and only once per OData GET method (per controller), which results in "stuck" (cached?) custom formatting.


I am working on a Web API project, and have implemented and registered my own JsonMediaTypeFormatter:

config.Formatters.Clear();
config.Formatters.Add(MyJsonFormatter);

'MyJsonFormatter' has custom implementations of the following:

 `-> SerializerSettings 
     `-> ContractResolver
         `-> CreateProperty

In my protected override CreateProperty(MemberInfo member, MemberSerialization memberSerialization) method, I restrict certain properties from being serialized based on user permissions.

This works great for all my API endpoints except for my OData enabled GET requests. Each controller has a GET method using the Primary Keys of the object, and an OData GET method which has a format similar to the following:

[HttpGet, Route]
public PageResult<Customer> GetOData(ODataQueryOptions<Customer> options) 
{
    IQueryable qCustomer = options.ApplyTo(_args.Context.Customers);
    return new PageResult<Customer>(qCustomer as IEnumerable<Customer>, Request.GetNextPageLink(), Request.GetInlineCount());
}

If I put a breakpoint on my overwritten CreateProperty method, it gets hit with every API request. However, it will only get hit once per OData GET method per controller. So a subsequent call from a different user with different permissions skips my code and gives me the formatting used in the first call.

If I restart the API, I can again hit the breakpoint (once), and get my formatting permissions for the user the call was made by, but subsequent calls (no matter the user) do not hit my breakpoint. Obviously, restarting the API for every OData request is not a solution I can live with.

I have put almost a full day into researching this, and have found several posts (here, here, here, etc.) which lead me to believe I need to implement my own ODataMediaTypeFormatter.

However, if this is the case, why is it hitting my JsonFormatter breakpoint? It seems like it uses my formatter, somehow caches my format permissions for that controller, and uses them from then on.

(Secondly, creating my own ODataFormatter does not seem to be a valid option anymore, since the codebase has apparently changed since this post - CreateEdmTypeSerializer does not exist. (I'm using Microsoft ASP.NET Web API 2.1 OData, version 5.1.2).)


Question: Is there a way I can get OData to play nicely with my JsonFormatter, and run through my custom CreateProperty code for each request?

If someone can at minimum explain what is going on here, it may help to point me in the direction I need to go, but right now my brain is just melting. :P


Update: I published to IIS and found that if I recycle the app pool, the formatting seems to be refreshed. So it definitely seems that something is being cached, the question is 'what' and 'why' - do PageResults automatically get cached? How do I stop whatever is being cached from being cached?


Solution

  • I don't know that my question was asked very well, as at the time I didn't entirely know what I was looking for or what was going wrong... However, since then, I have found an answer and figured I would post in just in case someone else runs into my issue.

    The issue I was having is that I need to not serialize specific properties in my webapi Json response based on the permissions of the caller. The problem was, the first call upon running the API worked fine, however subsequent calls were not hitting my breakpoints, and were being returned with the permissions of the first request.

    The resolution I found was to override another method in my ContractResolver to disable caching for the types I didn't want cached (in this case, anything with Entity as its base class).

    public class SecurityContractResolver : DefaultContractResolver
    {
        public override JsonContract ResolveContract(Type type)
        {
            if (type == null)
                throw new ArgumentNullException("type");
    
            if (type.IsSubclassOf(typeof(Entity)))
                return CreateContract(type);  //don't use cache in base method - we need different contract resolution per user / permissions
    
            return base.ResolveContract(type);  // <-- the base class calls CreateContract and then caches the contract
        }
        .....
    }
    

    Seems to be working great so far. Hope this helps someone!