How to create a response from a generic class containing HttpContext data in .Net 5 Web API

Viewed 641

I've started a new project using .Net 5 (my previous was .Net Framework 4.7). I'm writing a web API project and I want all my controllers/action responses to be of a certain type. This allows me to put some info I want included in every response, such as the current user info (and more stuff too). My generic response looks like this (I've only left the relevant code):

public class MyResponse<T>
{
    public T data { get; set; }
    public User user { get; set; }
    public MyResponse(T inputData)    
    {
        data = inputData;
    }
}

And I set the response on a controller's action this way:

public IActionResult Get()
{
    var response = new MyResponse<string>("Hello");
    return Ok(response);
}

So the idea is that the response always contains a "data" property with the actual data, and a bunch of other properties with metadata.

The problem is how to include information on the logged in user in .Net 5. In .Net 4.x you could just access HttpContext from anywhere, so you could just populate the User property. But this is not possible in .Net 5

I'm going crazy trying to understand how to achieve this in .Net 5.

The first thing I've tried is DI (which I'm new to, so I might not be understanding this properly).

The first thing I tried is to make my User class depend on IHttpContextAccessor as most documentation points to:

public class User : IIdentity

{
    private readonly IHttpContextAccessor _httpContextAccessor;
    public User(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
}

and register it this way on startup.cs:

services.AddHttpContextAccessor();
services.AddTransient<User>();

But that doesn't work well, since when I try to create my User class within MyResponse class:

var user = new User(); // This doesn't work, as the constructor requires one argument

So the constructor requires one argument so I can't create the class like that. I (believe) I would need to create the User from the DI container, but I don't have access to that on MyResponse class (or at least I couldn't really understand how to do it or if possible at all).

I could pass the HttpContext from the controller to MyResponse, but that seems plain wrong (plus, there might be other people writing controllers, so I think it's better if they don't explicitly need to pass that to the response, should be handled transparently)

My concrete questions:

Any thoughts of how can I get hold of the HttpContext within my custom response class? Should I be looking for an alternative option (such as a Middleware or Filter) to generate my response?

Thank you very much.

1 Answers

You could use a factory along with dependency injection.

Create your user class:

using Microsoft.AspNetCore.Http;
using System.Security.Principal;

public class User : IIdentity
{
    private IHttpContextAccessor HttpContextAccessor { get; }
    public User(IHttpContextAccessor httpContextAccessor)
    {
        this.HttpContextAccessor = httpContextAccessor;
    }

    public string AuthenticationType => this.HttpContextAccessor.HttpContext.User.Identity.AuthenticationType;

    public bool IsAuthenticated => this.HttpContextAccessor.HttpContext.User.Identity.IsAuthenticated;

    public string Name => this.HttpContextAccessor.HttpContext.User.Identity.Name;
}

Use DI to inject factories with the types you want:

services.AddHttpContextAccessor();
services.AddSingleton(a => GetResponse<string>(a));
services.AddSingleton(a => GetResponse<int>(a));
services.AddSingleton(a => GetResponse<decimal>(a));

Func<T, MyResponse<T>> GetResponse<T>(IServiceProvider serviceProvider)
{
    var contextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
    var user = new User(contextAccessor);

    return (data) => new MyResponse<T>(user, data);
}

Then inject it where you want:

namespace WebAppFiles.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class MyController : ControllerBase
    {
        private Func<int, MyResponse<int>> ResponseFactory { get; }

        public MyController(Func<int, MyResponse<int>> responseFactory)
        {
            this.ResponseFactory = responseFactory;
        }

        [HttpGet]
        public IActionResult Get([FromQuery] int value)
        {        
            return Ok(this.ResponseFactory(value));
        }
    }
}
Related