7

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);
Kiran
  • 56,921
  • 15
  • 176
  • 161
Kuba Matjanowski
  • 350
  • 3
  • 14

2 Answers2

2

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
Kiran
  • 56,921
  • 15
  • 176
  • 161
  • 3
    Seems there is no "InputFormatters" to "ResourceExecutingContext" on .net core. Then how could we get the formatters? – HappyLiang Nov 06 '17 at 19:55
  • Does not work in ASP.NET Core 2.0+. You would need to create a TypeFilter in order to instantiate an IActionFitler that uses Dependency Injection to grab the IOptions in the constructor. You can then modify InputFormatters and OutputFormatters in OnActionExecuting(). – parleer Jun 07 '18 at 16:38
  • @parleer won't that modify the options for all controllers? That could be a problem for concurrent requests, unless that options instance is a deep copy of the options, configuring behavior per-action. – dcstraw Sep 04 '18 at 16:03
  • FYI I found an alternative for .NET Core 2.0+ that shouldn't have a concurrency issue, and I answered here: https://stackoverflow.com/a/52193035/10391 – dcstraw Sep 05 '18 at 20:39
0

Interesting problem.

What about making the ReferenceResolver a facade:

    class ControllerReferenceResolverFacade : IReferenceResolver
    {
        private IHttpContextAccessor _context;

        public ControllerReferenceResolverFacade(IHttpContextAccessor context)
        {
            _context = context;
        }

        public void AddReference(object context, string reference, object value)
        {
          if ((string)_context.HttpContext.RequestServices.GetService<ActionContext>().RouteData.Values["Controller"] == "HomeController")
            {
                // pass off to HomeReferenceResolver
            }
            throw new NotImplementedException();
        }

Then you should be able to do:

services.AddMvc()
    .AddJsonOptions(options => options.SerializerSettings.ReferenceResolverProvider = () => {
        return new ControllerReferenceResolverFacade(
            services.BuildServiceProvider().GetService<IHttpContextAccessor>());
        });

This might not be exactly what you need but it might help you get started?

armen.shimoon
  • 6,303
  • 24
  • 32