May 7 2017

Asp.net core: Deserializing Json with Dependency Injection

Category: MVC | Asp.net core | Asp.netFrancesco @ 21:30

Custom Model Binding in Asp.net Core, 3: Model Binding Interfaces

In a previous post I discussed how to model bind Interfaces/abstract classes with the help of Asp.net core dependency injection features. However, the technique discussed there doesn’t apply to Json data added to the request body, since request body is processed by the BodyModelBinder that, in turn, selects a formatter based on the request content-type header. The formatter for “application/json” is a wrapper for Json.net, so the code of our previous “interfaces model binder” is never called, and also if called it wouldn’t be able to process Json data in the request body.

The behavior of Json.net serializer/deserializer may be customized through an MvcJsonOptions options object.In this post I am going to show how to modify Json.net behavior so that it attempts to use Dependency Injection to create any .Net object before creating it through its constructor. This, will enable not only the deserialization of interfaces, but also the customization of object creation through Asp.net Core DI engine.

Preparing the project

As a first step let create a new asp.net core project named “JsonDI”:

Project

Select “no authentication” to simplify the code created by VS project template.

Then let create a new folder called “Models” and add it the following “IPerson.cs” file, that contains both an interface definition and its default implementation:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace JsonDI.Models
{
    public interface IPerson
    {
       string Name { get; set; }
       string Surname { get; set; }
    }
    public class DefaultPerson: IPerson
    {
        public string Name { get; set; }
        public string Surname { get; set; }
    }
}

Now let modify the code of the Home controller to add an HttpPost for the Index action:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using JsonDI.Models;

namespace JsonDI.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
        [HttpPost]
        public IActionResult Index([FromBody] IPerson model)
        {
            return Json(model);
        }
        ....
        ....

We need an Ajax call to send Json data to our newly created HttpPost action method. Go to the Index.cshtml file in the Views/Home folder and substitute its code with the code below:

@{
    ViewData["Title"] = "Deserialization with DI";
}

<h2>@ViewData["Title"]</h2>

<form asp-action="Index" asp-controller="Home">
    
    <button data-url="@Url.Action("Index", "Home")" type="button" class="test btn btn-default">Submit</button>
</form>
@section Scripts
{
    <script type="text/javascript">
        $(".test").click(function (evt) {
            var url = $(evt.target).attr("data-url");
            $.ajax(url,
                {
                type: 'POST',
                data: JSON.stringify({
                    name: "Francesco",
                    surname: "Abbruzzese"
                }),
                success: function () { alert("message received"); },
                contentType: "application/json",
                dataType: 'json'
            });
        });
    </script>
}

Now place a breakpoint here:

Breakpoint

…and run the project.

When we click the “Submit” button an Ajax request is issued to our HomeController that contains an IPerson interface in the request body. However, when the breakpoint is hit, we may verify that the model parameter is null! In fact Json.net is not able to deserialize the IPerson interface because it doesn’t know how to create an instance of it.

Enriching Asp.net Core Dependency Injection.

Pieces of code that need Asp.net Core Dependency Injection engine creates type instances do it through the IServiceProvider interface that contains the unique method “GetService(Type t)”. Accordingly, we may create a type instance calling GetService or some other extension method that, in turn, calls it, but we are not able to ask if a type has been registered with the DI engine. This problem may be solved using a third party DI engine, such as Autofac. In this section we will show how to add this capability without using third party DI engines.

Let add an “Extensions” folder to the project. We will place there all extensions we need to have Json.net use DI. As a first step let define an interface containing all features we need and that are not exposed by IServiceProvider:

using System;

namespace JsonDI.Extensions
{
    public interface IDIMeta
    {
        bool IsRegistred(Type t);
        Type RegistredTypeFor(Type t);
    }
}

I called it IDIMeta because it provides meta-information about the content of the the DI container. The first method returns if a type has been registered with the DI engine, and the second returns the type that will be actually created when requiring an instance of the registered type.

The actual implementation of the above interface uses the IServiceCollection available in the ConfigureServices method of the Startup.cs class to collect all information needed to implement the interface methods in a dictionary :

using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;

namespace JsonDI.Extensions
{
    public class DIMetaDefault: IDIMeta
    {
        IDictionary<Type, Type> register = new Dictionary<Type, Type>();
        public DIMetaDefault(IServiceCollection services)
        {
            foreach (var s in services)
            {
                register[s.ServiceType] = s.ImplementationType;
            }
        }
        public bool IsRegistred(Type t)
        {
            return register.ContainsKey(t);
        }

        public Type RegistredTypeFor(Type t)
        {
            return register[t];
        }
    }
}

As a final step we need to register IDIMeta itself in the DI engine, by adding the code below to the ConfigureServices method of the Startup.cs class:

services.TryAddSingleton<IDIMeta>(s =>
{
    return new DIMetaDefault(services);
});

Later on we will show the whole code to be added to the ConfigureServices method of the Startup.cs class.

Customizing Json.net behavior

Now we are ready to customize Json.net behavior! The way Json.net serializes/deserializes types is specified by ContractResolvers. ContractResolvers return a “contract” for each type that contains all information on how to deal with that type. In particular each contract specifies how to create an instance of the type and how to fill its properties. The way to fill type properties may be customized by passing a custom converter in the “Converter” property of the contract. However, we don’t need custom converters since we must customize just the way types are created.

Our custom ContractResolver inherits form the built-in “CamelCasePropertyNamesContractResolver” that is the one used as a default by Asp.net Core:

using System;
using Newtonsoft.Json.Serialization;

namespace JsonDI.Extensions
{
    public class DIContractResolver: CamelCasePropertyNamesContractResolver
    {
        IDIMeta diMeta;
        IServiceProvider sp;
        public DIContractResolver(IDIMeta diMeta, IServiceProvider sp)
        {
            this.diMeta = diMeta;
            this.sp = sp;
        }
        protected override JsonObjectContract CreateObjectContract(Type objectType)
        {

            if (diMeta.IsRegistred(objectType))
            {
                JsonObjectContract contract = DIResolveContract(objectType);
                contract.DefaultCreator = () => sp.GetService(objectType);

                return contract;
            }

            return base.CreateObjectContract(objectType);
        }
        private JsonObjectContract DIResolveContract(Type objectType)
        {
            var fType = diMeta.RegistredTypeFor(objectType);
            if (fType != null) return base.CreateObjectContract(fType);
            else return CreateObjectContract(objectType);
        }
    }
}

We override the CreateObjectContract method to modify the custom contract. If the type our ContractResolver has been invoked on is registered in the DI container we call DIResolveContract that, if available, returns a contract for the type that will be actually created by the DI engine. If no type is specified DIResolveContract falls back on a contract for the original type (which might have less properties than the actual type created). After that CreateObjectContract changes the contract DefaultCreator property to use DI.

Our ContractResolver needs both IServiceProvider and IDIMeta to do its job, so we passed them in its constructor.

Installing our DIContractResolver

Our DIContractResolver may be installed by configuring the MvcJsonOptions options object. In my previous posts of the custom model binding series I showed how to configure options by passing a lambda function to various extension methods in the ConfigureServices method of the Startup.cs class. We may also factor out the configuration code for each specific option object in a class by writing a class that inherits from IOption<MyOptionObject>:

using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace JsonDI.Extensions
{
    public class JsonOptionsSetup: IConfigureOptions<MvcJsonOptions>
    {
        IServiceProvider serviceProvider;
        public JsonOptionsSetup(IServiceProvider serviceProvider)
        {
            this.serviceProvider = serviceProvider;
        }
        public void Configure(MvcJsonOptions o)
        {
            o.SerializerSettings.ContractResolver =
                new DIContractResolver(serviceProvider.GetService<IDIMeta>(), serviceProvider);
        }
    }
}

After that, it is enough to call “services.AddTransient<IConfigureOptions<MvcJsonOptions>, JsonOptionsSetup>();” in the ConfigureServices method of the Startup.cs class. Summing up the whole code of the ConfigureServices method of the Startup.cs class is:

public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddMvc();
            
            services.AddTransient<IPerson, DefaultPerson>();
            services.TryAddSingleton<IDIMeta>(s =>
            {
                return new DIMetaDefault(services);
            });
            services.AddTransient<IConfigureOptions<MvcJsonOptions>, JsonOptionsSetup>();
        }

For the above to compile properly we need the “usings” below:

using JsonDI.Extensions;
using JsonDI.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

 

That’s all! Run the project, click the submit button and wait on our previous breakpoint. This time the IPerson model contains the right data sent by the client!

Stay Tuned!

Francesco

Tags: , , , ,