简体   繁体   中英

Third-party implementation of DbContext, how to also implement IdentityDbContext

I'm working with Audit.NET , an open-source auditing framework, which provides an extension for Entity Framework and DbContext here: AuditDbContext.cs

// Implements DbContext public abstract partial class AuditDbContext : DbContext

I'd like to implement Audit.NET in my project using this Entity Framework extension because it would automate a lot of the steps I'd otherwise need to do manually (I'm able to use Audit.NET manually and without the Entity Framework extension). The problem I'm running into is that my solution repository implements IdentityDbContext which of course is an implementation of DbContext also.

// Implements IdentityDbContext public class MyDataContext : IdentityDbContext<ApplicationUser> { public MyDataContext() : base("DefaultConnection") { } ...

There is no existing AuditDbContext that implements IdentityDbContext .

I can't seem to think of a clean way to mix these two together and make my repository use AuditDbContext , especially given that AuditDbContext has protected constructors, and that both DbContext and IdentityDbContext have protected methods. I attempted to create a composition called AuditIdentityDbContext that had private copies of each context, but I'm not able to fulfill all of their interfaces by doing so.

It seems like all 3 DbContext types above need to be inherited from because of the protected members. For the first time in my career, I feel like multiple inheritance might actually help in this situation, but given that isn't a possibility, what would be the best alternative?

The only thing I can think of is creating a new class that inherits from either AuditDbContext or IdentityDbContext<TUser> and manually implement whatever is left to match the functionality of the other. There are no interface classes to implement though, so I'm pretty sure this isn't going to work. I feel like I must be overlooking something.

Ouch!

But you can still find a way out. There is nothing special about IdentityDbContext , there is even no interface there! So all you need to do is to tell your own DbContext about users and roles and add some validation.

The meat of IdentityDbContext is this:

    public virtual IDbSet<TUser> Users { get; set; }

    public virtual IDbSet<TRole> Roles { get; set; }

    public bool RequireUniqueEmail { get; set; } // this might not be important for you

Replace generic parameters with your own ApplicationUser and ApplicationRole (both of these classes can inherit from Identity-provided classes)

And add or blend with your own methods:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        if (modelBuilder == null)
        {
            throw new ArgumentNullException("modelBuilder");
        }

        // Needed to ensure subclasses share the same table
        var user = modelBuilder.Entity<TUser>()
            .ToTable("AspNetUsers");
        user.HasMany(u => u.Roles).WithRequired().HasForeignKey(ur => ur.UserId);
        user.HasMany(u => u.Claims).WithRequired().HasForeignKey(uc => uc.UserId);
        user.HasMany(u => u.Logins).WithRequired().HasForeignKey(ul => ul.UserId);
        user.Property(u => u.UserName)
            .IsRequired()
            .HasMaxLength(256)
            .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute("UserNameIndex") { IsUnique = true }));

        // CONSIDER: u.Email is Required if set on options?
        user.Property(u => u.Email).HasMaxLength(256);

        modelBuilder.Entity<TUserRole>()
            .HasKey(r => new { r.UserId, r.RoleId })
            .ToTable("AspNetUserRoles");

        modelBuilder.Entity<TUserLogin>()
            .HasKey(l => new { l.LoginProvider, l.ProviderKey, l.UserId })
            .ToTable("AspNetUserLogins");

        modelBuilder.Entity<TUserClaim>()
            .ToTable("AspNetUserClaims");

        var role = modelBuilder.Entity<TRole>()
            .ToTable("AspNetRoles");
        role.Property(r => r.Name)
            .IsRequired()
            .HasMaxLength(256)
            .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute("RoleNameIndex") { IsUnique = true }));
        role.HasMany(r => r.Users).WithRequired().HasForeignKey(ur => ur.RoleId);
    }

And

    protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry,
        IDictionary<object, object> items)
    {
        if (entityEntry != null && entityEntry.State == EntityState.Added)
        {
            var errors = new List<DbValidationError>();
            var user = entityEntry.Entity as TUser;
            //check for uniqueness of user name and email
            if (user != null)
            {
                if (Users.Any(u => String.Equals(u.UserName, user.UserName)))
                {
                    errors.Add(new DbValidationError("User",
                        String.Format(CultureInfo.CurrentCulture, IdentityResources.DuplicateUserName, user.UserName)));
                }
                if (RequireUniqueEmail && Users.Any(u => String.Equals(u.Email, user.Email)))
                {
                    errors.Add(new DbValidationError("User",
                        String.Format(CultureInfo.CurrentCulture, IdentityResources.DuplicateEmail, user.Email)));
                }
            }
            else
            {
                var role = entityEntry.Entity as TRole;
                //check for uniqueness of role name
                if (role != null && Roles.Any(r => String.Equals(r.Name, role.Name)))
                {
                    errors.Add(new DbValidationError("Role",
                        String.Format(CultureInfo.CurrentCulture, IdentityResources.RoleAlreadyExists, role.Name)));
                }
            }
            if (errors.Any())
            {
                return new DbEntityValidationResult(entityEntry, errors);
            }
        }
        return base.ValidateEntity(entityEntry, items);
    }

(taken from source code as is)

And when you need to create ApplicationUserManager it takes IUserStore and default implementation of user store takes DbContext so you can provide your own DbContext that actually inherit from AuditDbContext

This should do the trick without having to double-inherit. Viva open-source!

I had a similar problem with a project that I had worked on. What we ended up doing was doing two separate DbContext however they share the same database.

Take a look at this answer that shows you how to access Users and Roles properties, and Claims , Logins and UserRoles from the IdentityDbContext

Why is Asp.Net Identity IdentityDbContext a Black-Box?

@trailmax solution should work, but I've updated the Audit.NET library to include support for IdentityDbContext .

A new class AuditIdentityDbContext is provided so you can change your context to inherit from that one.

You should change your code to:

public class MyDataContext : AuditIdentityDbContext<ApplicationUser>
{
    ...
}

Note: I'm the library owner

GitHub Issue

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM