Dependency Injection on AuthorizationOptions Requirement in DotNet Core
Asked Answered
L

2

17

I have a .NET core project and am trying to create a custom policy using AuthorizationOptions as shown in the documentation located here:

ASP.NET.Core Authorization - Dependency Injection in requirement handlers

The examples show setting up an authorization requirement with 1 parameter - a simple int value. My custom requirement requires a string parameter as well as a DbContext object. I want to inject the DbContext into the requirement's constructor at runtime. I am using the Autofac container. I'm not sure how I can achieve this - have tried several approaches and nothing is working so far.

Here is my custom requirement:

public UserNameRequirement(string username, MyDbContext context)
{
    _userName = username;
    _dbContext = context;
}

When setting up the authorization options in Startup.cs ConfigureServices method the documentation shows you register this like so:

services.AddAuthorization(options =>
{
    options.AddPolicy(
        "UserNamePolicy",
        policy => policy.Requirements.Add(new UserNameRequirement("admin", ** want to resolve and inject my DbContext here **)));
}

I am not sure how to achieve this. I've seen this post which is a similar question but it's using ASP.NET 5 and that syntax doesn't work with .net core:

Dependency Injection on AuthorizationOptions

Lovell answered 23/2, 2017 at 18:17 Comment(2)
Please provide a Minimal, Complete, and Verifiable example.Hushhush
out of curiosity, what is the need for an instance of MyDbContext in the UserNameRequirement class?Woman
C
31

OK, I'm going to make an assumption here, and that is that you need to inject an instance of MyDbContext in UserNameRequirement to perform the business logic.

If this is the case, then it means UserNameRequirement both holds the data - in your case the username - and does the authorization logic. An example of this in ASP.NET Core is the ClaimsAuthorizationRequirement.

The solution to this is to separate this into two classes - on one side the requirement that just holds the data associated with the requirement, on on the other side the authorization handler. As a note, even if we'll go through it, what I'm describing is available in the official ASP.NET Core docs.

So the requirement class could look something like:

public class UserNameRequirement : IAuthorizationRequirement
{
    public UserNameRequirement(string userName)
    {
        UserName = userName;
    }

    public string UserName { get; }
}

and the handler class would be:

public class UserNameRequirementHandler : AuthorizationHandler<UserNameRequirement>
{
    private readonly MyDbContext _dbContext;

    public UserNameRequirementHandler(MyDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserNameRequirement requirement)
    {
        var userName = requirement.UserName;

        // Use _dbContext to perform business logic
    }
}

The next and last part is to register the handler in the container:

services.AddSingleton<IAuthorizationHandler, UserNameRequirementHandler>();

The effect of doing this is that you can now add your requirement to the policy without worrying about the DbContext:

services.AddAuthorization(options =>
{
    options.AddPolicy(
        "UserNamePolicy",
        policy => policy.Requirements.Add(new UserNameRequirement("admin")));
}

Internally, ASP.NET will then resolve all the handlers associated with that requirement through the container, so the instance of MyDbContext will be available to you in the handler, allowing you to perform the business logic as you see fit.

Hopefully, my assumption is correct, and this helps you.

Edit:

Henry Roux made a good point in a comment below regarding the fact that if the UserNameRequirementHandler is registered as a singleton, then a single instance of MyDbContext will be used, and that could lead to issues. Make sure you register your authorization handlers with the appropriate lifecycle.

Cloy answered 24/2, 2017 at 10:47 Comment(6)
Thank you! This is exactly the clear explanation I needed.Lovell
I'm interested in how successful this solution has held up to load testing and real-world production environments? The UserNameRequirementHandler is registered as a singleton, and this means that the MyDbContext that gets injected there is never refreshed. It is thus never disposed, the connection is never closed, and if you don't use the AsNoTracking() option, you'll find that data updated in the DB is never reported correctly when you read from that entity. Experienced any of this?Minneapolis
@HenkRoux that's a good point. I'll try to justify this by saying the answer was here to provide a way to use DI for requirements, not the one way to do it. I'll edit the answer to include your comments. cheers.Woman
@MickaëlDerriey what if I had multiple handlers that need DI? The snippet services.AddSingleton<IAuthorizationHandler, UserNameRequirementHandler>(); clearly links all IAuthorizationHandlers to that single instance...Obed
@Obed Not really, it links the UserNameRequirementHandler as an instance of IAuthorizationHandler, not as the single instance. The subtle difference is that you can register multiple types or instances against the same IAuthorizationHandler interface, and the framework will resolve all of them. I suggest that you read the official documentation for more information.Woman
Could the AddSingleton be used to substitue in a new IOptions in here: github.com/aspnet/AspNetCore/blob/…Sumpter
P
1

You can use also the GetRequiredService method:

public class ExampleRequirement : AuthorizationHandler<ExampleRequirement>, IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ExampleRequirement requirement)
    {
        UserManager<ApplicationUser> UserManager = ((ActionContext)context.Resource).HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();

        // you can work with the users ...      

        return Task.CompletedTask;
    }
}
Plainspoken answered 31/1, 2020 at 20:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.