Moving ApplicationUser and other models out of MVC

2019-01-17 13:02发布

问题:

How can I split the properties, functionality and classes out of the default ASP.Net Mvc / Identity 2.0? I am battling with a few things:

  • by default, it wants to use OWIN's context to wire up some kind of dependency injection, and control the Managers
  • it puts the ApplicationDbContext at the application level, where my architecture requires it be available at "lower" levels.
  • It requires that I declare any properties in the same class as the functionality that acts on those properties (which doesn't fit in with my architecture)
  • The ApplcationUser model has dependencies on Asp.Net, which I would like to break if I am to move the POCO to a non-MVC layer of the solution

Application architecture:

I have a solution that has several tiers :

  • Api - defines interfaces for services
  • Domain - stores POCO models representing a business domain
  • Business - stores logic for interacting with domain objects, and consumes services
  • Service - implementation of services, including Entity Framework, and the structure maps for the domain objects
  • Application - in this case, an MVC application.

My business layer only knows about service interfaces, not implementation, and I am using dependency injection to wire everything up.

I have some interfaces defining read/write/unit of work operations for a data service, and an implementation of these that inherit from DbContext (in my Service layer). Instead of having a series of DbSet<MyPoco> MyPocos {get;set;}, I am wiring it up by passing a series of type configurations that define relationships, then accessing my types through Set<Type>(). All that works great.

This stack has been in place for an existing application, and works well. I know it will transition to an MVC application, and am only having issues with the "out of the box" ASP.Net Identity-2.

回答1:

My solution to this was to: Abstract all the things

I got around this by abstracting most of the functionality of identity into its own project which allowed for easier unit testing and reuse of the abstraction in other projects.

I got the idea after reading this article

Persistence-Ignorant ASP.NET Identity with Patterns

I then fine tuned the idea to suit my needs. I basically just swapped out everything I needed from asp.net.identity for my custom interfaces which more or less mirrored the functionality provided by the framework but with the advantage of easier abstractions and not implementations.

IIdentityUser

/// <summary>
///  Minimal interface for a user with an id of type <seealso cref="System.String"/>
/// </summary>
public interface IIdentityUser : IIdentityUser<string> { }
/// <summary>
///  Minimal interface for a user
/// </summary>
public interface IIdentityUser<TKey>
    where TKey : System.IEquatable<TKey> {
    TKey Id { get; set; }
    string UserName { get; set; }
    string Email { get; set; }
    //...other code removed for brevity
}

IIdentityManager

/// <summary>
/// Exposes user related api which will automatically save changes to the UserStore
/// </summary>
public interface IIdentityManager : IIdentityManager<IIdentityUser> { }
/// <summary>
/// Exposes user related api which will automatically save changes to the UserStore
/// </summary>
public interface IIdentityManager<TUser> : IIdentityManager<TUser, string>
    where TUser : class, IIdentityUser<string> { }
/// <summary>
/// Exposes user related api which will automatically save changes to the UserStore
/// </summary>
public interface IIdentityManager<TUser, TKey> : IDisposable
    where TUser : class, IIdentityUser<TKey>
    where TKey : System.IEquatable<TKey> {
    //...other code removed for brevity
}

IIdentityResult

/// <summary>
/// Represents the minimal result of an identity operation
/// </summary>
public interface IIdentityResult : System.Collections.Generic.IEnumerable<string> {
    bool Succeeded { get; }
}

In my default implementation of the identity manager, which also lives in its own project, I simply wrapped the ApplicationManager and then mapped results and functionality between my types and the asp.net.identity types.

public class DefaultUserManager : IIdentityManager {
    private ApplicationUserManager innerManager;

    public DefaultUserManager() {
        this.innerManager = ApplicationUserManager.Instance;
    }
    //..other code removed for brevity
    public async Task<IIdentityResult> ConfirmEmailAsync(string userId, string token) {
        var result = await innerManager.ConfirmEmailAsync(userId, token);
        return result.AsIIdentityResult();
    }
    //...other code removed for brevity
}

The application layer is only aware of the abstractions and the implementation is configured at startup. I don't have any using Microsoft.AspNet.Identity at the higher level as they are all using the local abstractions.

the tiers can look like this :

  • Api - defines interfaces for services (including Identity abstraction interfaces)
  • Domain - stores POCO models representing a business domain
  • Business - stores logic for interacting with domain objects, and consumes services
  • Service - implementation of services, including Entity Framework, and the structure maps for the domain objects
  • Identity - implementation of Microsoft.AspNet.Identity specific services, including Microsoft.AspNet.Identity.EntityFramework; and OWIN configuration
  • Application - in this case, an MVC application.

In MVC Application layer the AccountController thus only needed

using MyNamespace.Identity.Abstractions

public partial class AccountController : Controller {
    private readonly IIdentityManager userManager;

    public AccountController(IIdentityManager userManager) {
        this.userManager = userManager;
    }

    //...other code removed for brevity

    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Signin(LoginViewModel model, string returnUrl) {
        if (ModelState.IsValid) {
            // authenticate user
            var user = await userManager.FindAsync(model.UserName, model.Password);
            if (user != null) {
                //...code removed for brevity
            } else {
                // login failed
                setFailedLoginIncrementalDelay();
                ModelState.AddModelError("", "Invalid user name or password provided.");
            }
        }
        //TODO: Audit failed login

        // If we got this far, something failed, redisplay form
        return View(model);  
    }
}

This assumes you are using some DI framework. It is only in the configuring of the IoC that any mention is made of the layer that implements identity completely abstracting it away from those that have need of using identity.

//NOTE: This is custom code.
protected override void ConfigureDependencies(IContainerBuilder builder) {
    if (!builder.HasHandler(typeof(IIdentityManager))) {
        builder.PerRequest<IIdentityManager, DefaultUserManager>();
    }
}