3

I have a multi-layer application that I started writing in ASP.NET Core 1.1 which I'm still learning along the way. I have organized it like previous apps I've done in the Web API, I have host service (net core app), business layer and data layer that is above database. Business and data layers were net core standard libraries, but when I wanted to add entity framework I had to modify data layer to look like net core app, so now I have Startup.cs with configurations there. That allowed me to configure entity framework service and to create migrations in the data layer. But now I have a problem as I wanted to add asp.net identity. Every tutorial on the net is about SPAs that have everything in one project. I have added identity to Startup.cs and database is generated well

public void ConfigureServices(IServiceCollection services)
{
    var connectionString = Configuration.GetConnectionString("DefaultConnection");
    services.AddEntityFramework(connectionString);
    services.AddMyIdentity();
    services.Configure<IdentityOptions>(options =>
    {
        // Password settings
        options.Password.RequireDigit = true;
        options.Password.RequiredLength = 8;
        options.Password.RequireNonAlphanumeric = false;
        options.Password.RequireUppercase = true;
        options.Password.RequireLowercase = false;

        // Lockout settings
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
        options.Lockout.MaxFailedAccessAttempts = 10;


        // User settings
        options.User.RequireUniqueEmail = true;
    });
}
public void Configure(IApplicationBuilder app)
{
    app.UseIdentity();
}

but now I need to use UserManager from a class that is not a Controller and I don't know how to deal with dependency injection. To explain better, I have an Account controller in my Host Service

[HttpPost]
[Route("Register")]
public async Task<IActionResult> Register([FromBody]RegisterUserDto dto)
{
    var result = await Business.Commands.Accounts.Register(dto);
    return Ok(result);
}

Business layer just calls the Data layer

public async static Task<ResponseStatusDto> Register(RegisterUserDto dto)
{
    // some code here        
    var identityLogon = await Data.Commands.ApplicationUsers.Register(dto);
    // some code here as well

    return new ResponseStatusDto();
}

Now the question is, how do I get UserManager in the Data Register method? It's a simple class, it doesn't inherit from a controller, dependency injection is not working for constructors like in the examples found here Core Identity

public class AccountController : Controller
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly IEmailSender _emailSender;
    private readonly ISmsSender _smsSender;
    private static bool _databaseChecked;
    private readonly ILogger _logger;

    public AccountController(
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager,
        IEmailSender emailSender,
        ISmsSender smsSender,
        ILoggerFactory loggerFactory)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _emailSender = emailSender;
        _smsSender = smsSender;
        _logger = loggerFactory.CreateLogger<AccountController>();
    }

    //
    // GET: /Account/Login

So, how do I pass UserManager that is configured in Startup to some random class somewhere in the middleware? I have seen this question, but the answer to just pass null values to UseManager constructor is not working nor I think it's good.

//EDIT as per Set's answer

I have removed all static references, but I'm still not quite there. I have followed this dependency injection instructions, but I'm not sure how to instantiate and call Add method.

I have created an interface

public interface IIdentityTransaction
{
    Task<IdentityResult> Add(ApplicationUser appUser, string password);
}

and implemened it

public class IdentityTransaction : IIdentityTransaction
{
    private readonly ApplicationDbContext _dbContext;

    private readonly UserManager<ApplicationUser> _userManager;
    private readonly RoleManager<IdentityRole> _roleManager;


    public IdentityTransaction(ApplicationDbContext context, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)
    {
        _roleManager = roleManager;
        _userManager = userManager;
        _dbContext = context;
    }

    public async Task<IdentityResult> Add(ApplicationUser applicationUser, string password)
    {
        return await _userManager.CreateAsync(applicationUser, password);
    }

}

then I injected it to a service collection in Startup.cs

services.AddScoped<IIdentityTransaction, IdentityTransaction>();

but how to call Add method from IdentityTransaction service?

enter image description here

I cannot instantiate it nor use dependency injection on constructor as it just loops my problem. @Set mentioned

or pass UserManager userManager as parameter to method pass it from where?

I think I'm very close, but I'm missing something. I have tried using

    IIdentityTransaction it = services.GetRequiredService<IIdentityTransaction>();

but services which is IServiceProvider is null, I don't know where to get it from either.

Community
  • 1
  • 1
Милан
  • 195
  • 1
  • 3
  • 9

2 Answers2

2

DI in ASP.NET Core works the same for controller and non-controller classes using "constructor injection" approach.

You have the problem as Register method is static, so doesn't have access to instance variables/properties. You need to

  • make Register method non-static
  • or pass UserManager<ApplicationUser> userManager as parameter to method

In general, you should avoid using static classes for business logic as they don't help to test your code properly and produce the code coupling. Search via internet/SO and you will find a lot of topics why static is bad.

Use DI to get the instance of Data.Commands.ApplicationUsers class in your controller. If you need only one instance of this class for your application - use singleton lifetime for it.


Update. Again, use constructor injection: modify your "Data Layer" class so it can get the instance of IIdentityTransaction as constructor parameter:

public class YourDataLayerClass : IYourDataLayerClass
{
    private IIdentityTransaction _identityTransaction;
    public YourDataLayerClass(IIdentityTransaction identityTransaction)
    {
       _identityTransaction = identityTransaction;
    }

    public void MethodWhereYouNeedToCallAdd()
    {
        _identityTransaction.Add(...);
    }
}

And idea the same for IYourDataLayerClass instance: register dependency

services.AddScoped<IYourDataLayerClass, YourDataLayerClass>();

and then the class (middleware in your case, if I understand you properly) that depends on it should receive that instance via constructor:

public class YourMiddleware
{
    private IYourDataLayerClass _yourDataLayerClass;
    public YourMiddleware(IYourDataLayerClass yourDataLayerClass)
    {
       _yourDataLayerClass = yourDataLayerClass;
    }
    ...
}
Set
  • 47,577
  • 22
  • 132
  • 150
  • @Pierre Murasso gave a similar answer, but I'm not sure I fully understand it. If I follow your proposal then the Business layer trying to instantiate YourMiddleware class will ask me to provide IYourDataLayerClass for the constructor. And I don't have it in Business layer. I add it as a constructor param in Business, then Host Account Controller needs to provide it? Then what? I add a reference to Data Layer in Host, configure it in Host Startup and push from there? – Милан Mar 12 '17 at 12:04
1

Yes you are very close.

First thing, either remove context parameter from the IdentityTransaction constructor as in your code snipped it appears to be useless. Or if you plan to use it later, declare it in the DI container:

services.AddScoped<ApplicationDbContext, ApplicationDbContext>();

Second thing, you simply need to add IIdentityTransaction as a dependency in the controller's constructor, and remove SignInManager and UserManager from its dependencies as eventually you won't use these directly within the controller:

public class AccountController : Controller
{
    private readonly IEmailSender _emailSender;
    private readonly ISmsSender _smsSender;
    private static bool _databaseChecked;
    private readonly ILogger _logger;
    IIdentityTransaction _identityTransaction;

    public AccountController(
        IEmailSender emailSender,
        ISmsSender smsSender,
        ILoggerFactory loggerFactory,
        IIdentityTransaction identityTransaction)
    {
        _emailSender = emailSender;
        _smsSender = smsSender;
        _logger = loggerFactory.CreateLogger<AccountController>();
        _identityTransaction = identityTransaction;
    }

If you need an additional business layer (IBusinessLayer) between the controller, same process, declare the class in the DI container at startup, add IIdentityTransaction as a dependency in the business class constructor, and update the controller's dependencies from IIdentityTransaction to IBusinessLayer.

A couple more precisions.

services.AddScoped<IIdentityTransaction, IdentityTransaction>();

This piece of code does NOT inject instances or dependencies. It declares an interface and its associated implementation in the DI container, so it can be injected later when required. Actual instances are injected when the objects that required them are actually created. I.e. the controller gets its dependencies injected when it is instantiated.

 IIdentityTransaction it = services.GetRequiredService<IIdentityTransaction>();

What you tried to do here is called the dependency locator pattern, and is often considered as an anti-pattern. You should stick to dependency injection via the constructor, it's much cleaner.

The key is to declare everything in the DI container at startup, even your custom business/data layers classes, never instantiate them yourself anymore, and declare them as required dependencies in any classes' constructor that need them.

Pierre Murasso
  • 591
  • 7
  • 14
  • Let's see if I understand this correctly. I have 3 layers, Host which is a real NetCore app with startup that I'm starting when debugging. Business layer which is Net Standard Library and a Data layer which was library, but is now "pseudo Core app" with Startup.cs because I followed this http://www.michael-whelan.net/ef-core-101-migrations-in-separate-assembly/ My app.UseIdentity is in the Data layer Startup. Host doesn't have reference to Data layer, only Business. If I understood you correctly you proposed that I start passing down my references from Account Controller in Host? – Милан Mar 12 '17 at 11:59
  • 1
    I propose the each layer declares the dependencies it requires in its constructor: Controller --> Business, Business --> DataLayer. And interfaces/implementations are declared in the DI container in the host app's startup.cs. – Pierre Murasso Mar 12 '17 at 12:02
  • You should set up everything in the host's startup.cs. – Pierre Murasso Mar 12 '17 at 12:05
  • ApplicationDbContext "must" be declared in the Data layer because there are issues with referencing data models from a different assembly. At least I was getting issues until I followed the above mentioned tutorial. I don't think that I should have a reference from Host to Data (I didn't when I was building similar projects in Web API), but if it's a workaround I need to implement at this stage, then it's OK. – Милан Mar 12 '17 at 12:09
  • 1
    If you are talking about the assembly reference, there is one indirectly because the reference graph is Host --> Business --> Data, and the data layer DLL will end up in the host's bin so it can be loaded at runtime. The behavior was the same before, even with Web API. – Pierre Murasso Mar 12 '17 at 12:13
  • @PierreMurasso could you advise where IEmailSender and ISmsSender are derived from? I am searching and cannot for the life of me find an example or piece of documentation that references the Nuget package or namespace they come from. – Tyler Durden Apr 21 '17 at 07:03
  • Why do you assume they come from a Nuget package? I guess they don't. He defined both interfaces (EmailSender and ISmsSender), and their implementations. – Pierre Murasso Apr 21 '17 at 09:53