.NET Core Set ClaimsPrincipal from another controller

Viewed 57

In few places in legacy code (more than 100 controllers), we are running action from other controllers.

In .NET Framework it runs OK - ClaimsPrincipal in both controller's action have correct values, but in .NET Core, running SecondController.internalPut() from FirstController gives me NullReferenceException.

FirstController:

[EnableCors]
public class FirstController : BaseApiController
{
    public FirstController(IContextFactory contextFactory) : base(contextFactory)
    {
    }

    [HttpPut]
    [HttpPost]
    [Route("/api/firstcontroller")]
    public IActionResult Put([FromBody] MyDTO data)
    {
        var token = Identity.Token; // <--- correct value

        var secondController = new SecondController(ContextFactory);
        secondController.internalPut(something); <--- NullReferenceException

        return Ok();
    }
}

SecondController:

[EnableCors]
public class SecondController : BaseApiController
{
    public SecondController(IContextFactory contextFactory) : base(contextFactory)
    {
    }

    [HttpPut]
    [HttpPost]
    public async Task<IActionResult> Put(Guid myGuid)
    {
        internalPut(something); // <-- OK
        return Ok();
    }

    internal void internalPut(object something)
    {
        var token = Identity.Token; // <--- NullReferenceException when running from FirstController!!
    }
}

And BaseApiController with TokenIdentity:

[ApiController]
[Route("/api/[controller]")]
[Route("/api/[controller]/[action]")]
public class BaseApiController : ControllerBase
{
    protected readonly IMyContextFactory ContextFactory;

    public BaseApiController(IMyContextFactory contextFactory)
    {
        ContextFactory = contextFactory;
    }

    public TokenIdentity Identity => User?.Identity as TokenIdentity;
}

public class TokenIdentity : GenericIdentity
{
    public Guid Token { get; set; }
    public string User { get; set; }

    public TokenIdentity(Guid token) : base(token.ToString())
    {
        Token = token;
    }
}

How is the easiest fix for this bug? I know that I can change BaseApiController implementation to get ClaimsPrincipal from IHttpContextAccessor, but this means that I need to update constructors for all > 100 controllers in code... It is another way to always have ClaimsPrincipal when we are calling action from another controller?

1 Answers

What I recommend as the correct solution

I can't emphasise enough how much I recommend moving shared functionality into its own services, or perhaps look at using the Mediator Pattern (e.g. using the MediatR library) to decouple your controllers from their functionality a little. What I provide below is not a solution, but a band-aid.

What I recommend a QUICK FIX only

Why is this only a quick fix?: because this doesn't instantiate the correct action details and route parameters, so it could potentially cause you some hard-to-find bugs, weird behaviour, URLs maybe not generating correctly (if you use this), etc.

Why am I recommending it?: because I know that sometimes time is not on our side and that perhaps you need a quick fix to get this working while you work on a better solution.

Hacky quick fix

You could add the following method to your base controller class:

private TController CreateController<TController>() where TController: ControllerBase
{
    var actionDescriptor = new ControllerActionDescriptor()
    {
        ControllerTypeInfo = typeof(TController).GetTypeInfo()
    };

    var controllerFactory = this.HttpContext.RequestServices.GetRequiredService<IControllerFactoryProvider>().CreateControllerFactory(actionDescriptor);
    return controllerFactory(this.ControllerContext) as TController;
}

Then instead of var secondController = new SecondController(ContextFactory); you would write:

var secondController = CreateController<SecondController>();
Related