Friday, February 9, 2018

Handling JSON errors in OWIN Web application

Somу time ago I wrote an article, where I discuss how to find typos and usage of obsolete properties in JSON body of a request. The described technique required providing a handler of Error event of the JsonSerializerSettings object. The problem is that this object is shared across all requests to your application, but we need separate handling for each request. In this short article, I'll describe how to do it.

First of all, I assume, that you are familiar with the previous article, because I'll use the same model and classes here. You may consider this article as a continuation of the previous one.

Let's take a look at how we usually define Web API application with JSON serialization:

[assembly: OwinStartup(typeof(JsonOwinWebApplication.Startup))]

namespace JsonOwinWebApplication
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            HttpConfiguration config = new HttpConfiguration();

            config.Formatters.Clear();
            config.Formatters.Add(
                new JsonMediaTypeFormatter {SerializerSettings = GetJsonSerializerSettings()});

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new {id = RouteParameter.Optional}
            );

            app.UseWebApi(config);
        }

        private static JsonSerializerSettings GetJsonSerializerSettings()
        {
            var settings = new JsonSerializerSettings();

            settings.Converters.Add(new StringEnumConverter { CamelCaseText = false });

            settings.ContractResolver = new CamelCasePropertyNamesContractResolver();

            return settings;
        }

    }
}

In our case, we need more complex configuration of JSON serializer. Take a look at the following pseudo code:

private static JsonSerializerSettings GetJsonSerializerSettings()
{
    var settings = new JsonSerializerSettings
    {
        MissingMemberHandling = MissingMemberHandling.Error,
        Error = (sender, args) =>
        {
            handler.Handle(sender, args);
        }
    };

    settings.Converters.Add(new StringEnumConverter { CamelCaseText = false });
    settings.Converters.Add(new ValueJsonConverterWithPath(pathsStack));

    settings.ContractResolver = new ObsoletePropertiesContractResolver();

    return settings;
}

Here handler and pathsStack should be different for each request. Moreover, the handler should have a reference to the corresponding pathsStack. Also, we must have access to the handler instance in our Web API methods, to get the list of typos and obsolete properties for the current request.

The problem here is that our Startup class configures JSON serializer for all requests, but we need it somehow different for each request. How can we solve this problem?

The first idea I had was about an inversion of control container. I knew, that I can configure lifespan of some objects as "per request". I thought, that everywhere I need unique objects for a request, I'll take them from such a container. Unfortunately, I was not able to implement it. I used Autofac container, I configured it to provide my handler and pathsStack objects per request, I attached the container to the OWIN pipeline and to the HttpConfiguration dependency resolver. But still, I got an error trying to get my objects from the container inside the Error handler of the JsonSerializerSettings. I'm not a big specialist in work with IoC containers, it may be that I made some mistake, so you are welcome to try.

Still, I had a task to solve. At that moment I thought about OWIN specification. It is very simple and elegant. It describes how your Web application communicates with hosting Web server. The Web server provides your application with an instance of IDictionary<string, object>. The Web server guarantees that the dictionary contains some data (host, path, query, request body, ...). Your application passes the dictionary through the pipeline of middleware. On each stage, you are free to add/remove/modify the content of the dictionary. In the end, your application must add to the dictionary some information about the response (headers, cookies, body, ...). That's it. And this process is repeated for each request.

And here comes my idea. In the beginning of the pipeline, I'll add my objects handler and pathsStack to the dictionary. Later I'll take them from the dictionary everywhere I need them. Here is how it works:

[assembly: OwinStartup(typeof(JsonOwinWebApplication.Startup))]

namespace JsonOwinWebApplication
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.Use(async (context, next) =>
            {
                var pathsStack = new Stack<string>();
                var handler = new TyposAndObsoleteHandlerWithPath(pathsStack);
                handler.Ignore(e => e.CurrentObject is Value && e.ErrorContext.Member.ToString() == "type");
                handler.AddObsoleteMessage((type, name) =>
                {
                    if (type == typeof(StringValue) && name == "Id")
                        return "Use another property here";
                    return null;
                });

                context.Set("My.PathsStack", pathsStack);
                context.Set("My.TyposHandler", handler);

                await next.Invoke();
            });

            HttpConfiguration config = new HttpConfiguration();

            config.Formatters.Clear();
            config.Formatters.Add(
                new JsonMediaTypeFormatter {SerializerSettings = GetJsonSerializerSettings()});

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new {id = RouteParameter.Optional}
            );

            app.UseWebApi(config);
        }

        private static JsonSerializerSettings GetJsonSerializerSettings()
        {
            var settings = new JsonSerializerSettings
            {
                MissingMemberHandling = MissingMemberHandling.Error,
                Error = (sender, args) =>
                {
                    var owinContext = HttpContext.Current.GetOwinContext();

                    var handler = owinContext.Get<TyposAndObsoleteHandlerWithPath>("My.TyposHandler");

                    handler.Handle(sender, args);
                }
            };

            settings.Converters.Add(new StringEnumConverter { CamelCaseText = false });
            settings.Converters.Add(new ValueJsonConverterWithPath(() =>
            {
                var owinContext = HttpContext.Current.GetOwinContext();

                var pathsStack = owinContext.Get<Stack<string>>("My.PathsStack");

                return pathsStack;
            }));

            settings.ContractResolver = new ObsoletePropertiesContractResolver();

            return settings;
        }

    }
}

In OWIN middleware I create my handler and pathsStack objects, configure them and add them to the OWIN context using context.Set method. Notice, that the constructor of the ValueJsonConverterWithPath class does not accept an instance of paths stack any more, but rather a function, which can return this instance. Again the reason is that pathsStack object should be different for each request.

Later in any place of my application, I can get OWIN context of the current request using HttpContext.Current.GetOwinContext(). I can use it in my Web API method to get access to the handler object to collect all found typos and usages of obsolete properties:

namespace JsonOwinWebApplication
{
    public class ValuesController : ApiController
    {
        public object Post([FromBody] Value[] values)
        {
            var owinContext = HttpContext.Current.GetOwinContext();

            var handler = owinContext.Get<TyposAndObsoleteHandlerWithPath>("My.TyposHandler");

            // Other processing...

            return new
            {
                Message = "Message processed",
                Warnings = handler.Messages.ToArray()
            };
        }
    }
}

That's it. I hope you'll find this tip useful for the described use case and for other cases, where you need to have separate objects for each Web request.

No comments:

Post a Comment