Search code examples
c#asp.net.netasp.net-corehateoas

Creating per-request controller/action based formatters in ASP.NET 5


I'm trying to implement HATEOAS in my ASP rest API, changing the ReferenceResolverProvider.

The problem is, that depending on which controller I use, I'd like to use different ReferenceResolvers, because I need to behave differently for each Controller.

Now I have universal options:

services.AddMvc()
            .AddJsonOptions(option => option.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver())
            .AddJsonOptions(options => options.SerializerSettings.ReferenceResolverProvider = () => new RoomsReferenceResolver<Room>())
            .AddJsonOptions(options => options.SerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.Objects);

And I want to have something like this:

services.AddMvc()
            .AddJsonOptions(option => option.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver())
            .AddJsonOptions<RoomsController>(options => options.SerializerSettings.ReferenceResolverProvider = () => new RoomsReferenceResolver<Room>())
            .AddJsonOptions(options => options.SerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.Objects);

Solution

  • You seem to be wanting to create a per-controller specific formatters. This can be achieved by using a filter called IResourceFilter. A quick example:

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class CamelCaseJsonFormatterResourceFilter : Attribute, IResourceFilter
    {
        private readonly JsonSerializerSettings serializerSettings;
    
        public CamelCaseJsonFormatterResourceFilter()
        {
            // Since the contract resolver creates the json contract for the types it needs to deserialize/serialize,
            // cache it as its expensive
            serializerSettings = new JsonSerializerSettings()
            {
                ContractResolver = new CamelCasePropertyNamesContractResolver()
            };
        }
    
        public void OnResourceExecuted(ResourceExecutedContext context)
        {
    
        }
    
        public void OnResourceExecuting(ResourceExecutingContext context)
        {
            // remove existing input formatter and add a new one
            var camelcaseInputFormatter = new JsonInputFormatter(serializerSettings);
            var inputFormatter = context.InputFormatters.FirstOrDefault(frmtr => frmtr is JsonInputFormatter);
            if (inputFormatter != null)
            {
                context.InputFormatters.Remove(inputFormatter);
            }
            context.InputFormatters.Add(camelcaseInputFormatter);
    
            // remove existing output formatter and add a new one
            var camelcaseOutputFormatter = new JsonOutputFormatter(serializerSettings);
            var outputFormatter = context.OutputFormatters.FirstOrDefault(frmtr => frmtr is JsonOutputFormatter);
            if (outputFormatter != null)
            {
                context.OutputFormatters.Remove(outputFormatter);
            }
            context.OutputFormatters.Add(camelcaseOutputFormatter);
        }
    }
    
    // Here I am using the filter to indicate that only the Index action should give back a camelCamse response
    public class HomeController : Controller
    {
        [CamelCaseJsonFormatterResourceFilter]
        public Person Index()
        {
            return new Person() { Id = 10, AddressInfo = "asdfsadfads" };
        }
    
        public Person Blah()
        {
            return new Person() { Id = 10, AddressInfo = "asdfsadfads" };
        }
    

    If you are curious about the filter execution order, following is an example of the sequence of them:

    Inside TestAuthorizationFilter.OnAuthorization
    Inside TestResourceFilter.OnResourceExecuting
    Inside TestActionFilter.OnActionExecuting
    Inside Home.Index
    Inside TestActionFilter.OnActionExecuted
    Inside TestResultFilter.OnResultExecuting
    Inside TestResultFilter.OnResultExecuted
    Inside TestResourceFilter.OnResourceExecuted