Zamieszanie DbContext tożsamości ASP.NET

196

Domyślna aplikacja MVC 5 zawiera ten fragment kodu w IdentityModels.cs - ten fragment kodu dotyczy wszystkich operacji związanych z tożsamością ASP.NET dla domyślnych szablonów:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext()
        : base("DefaultConnection")
    {
    }
}

Jeśli rusztuję nowego kontrolera za pomocą widoków z Entity Framework i utworzę „Nowy kontekst danych ...” w oknie dialogowym, otrzymam to dla mnie:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;

namespace WebApplication1.Models
{
    public class AllTheOtherStuffDbContext : DbContext
    {
        // You can add custom code to this file. Changes will not be overwritten.
        // 
        // If you want Entity Framework to drop and regenerate your database
        // automatically whenever you change your model schema, please use data migrations.
        // For more information refer to the documentation:
        // http://msdn.microsoft.com/en-us/data/jj591621.aspx

        public AllTheOtherStuffDbContext() : base("name=AllTheOtherStuffDbContext")
        {
        }

        public System.Data.Entity.DbSet<WebApplication1.Models.Movie> Movies { get; set; }

    }
} 

Jeśli rusztuję inny kontroler + widok za pomocą EF, powiedzmy na przykład dla modelu Animal, ta nowa linia zostanie automatycznie wygenerowana pod public System.Data.Entity.DbSet<WebApplication1.Models.Movie> Movies { get; set; }- tak jak poniżej:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;

namespace WebApplication1.Models
{
    public class AllTheOtherStuffDbContext : DbContext
    {
        // You can add custom code to this file. Changes will not be overwritten.
        // 
        // If you want Entity Framework to drop and regenerate your database
        // automatically whenever you change your model schema, please use data migrations.
        // For more information refer to the documentation:
        // http://msdn.microsoft.com/en-us/data/jj591621.aspx

        public AllTheOtherStuffDbContext() : base("name=AllTheOtherStuffDbContext")
        {
        }

        public System.Data.Entity.DbSet<WebApplication1.Models.Movie> Movies { get; set; }
        public System.Data.Entity.DbSet<WebApplication1.Models.Animal> Animals { get; set; }

    }
} 

ApplicationDbContext(dla wszystkich elementów tożsamości ASP.NET) dziedziczy, z IdentityDbContextktórych z kolei dziedziczy DbContext. AllOtherStuffDbContext(dla moich własnych rzeczy) dziedziczy DbContext.

Więc moje pytanie brzmi:

Którego z tych dwóch ( ApplicationDbContexti AllOtherStuffDbContext) powinienem używać dla wszystkich innych moich modeli? A może powinienem po prostu użyć domyślnej funkcji automatycznego generowania, ApplicationDbContextponieważ nie powinno to stanowić problemu, ponieważ pochodzi ona z klasy podstawowej DbContext, czy też będzie jakiś narzut? Należy używać tylko jeden DbContextobiekt w swojej aplikacji dla wszystkich modeli (czytałem to gdzieś), więc nie powinien nawet rozważyć zastosowanie zarówno ApplicationDbContexti AllOtherStuffDbContextw jednej aplikacji? Lub jaka jest najlepsza praktyka w MVC 5 z ASP.NET Identity?

Kot w butach
źródło
1
Tak poza tym; to jest super i jest niepotrzebne dla moich oczu podczas skanowania dokumentu: public System.Data.Entity.DbSet <WebApplication1.Models.Movie> Movies {get; zestaw; } - część System.Data.Entity i WebApplication1.Models. Czy nie można go usunąć z deklaracji i zamiast tego dodać przestrzenie nazw w sekcji instrukcji korzystania?
PussInBoots,
Kot - tak dla twojego komentarza. To powinno działać dobrze.
SB2055
Jest to dobry i działający przykład (MVC 6) i biblioteka implementacji z platformą ASP.NET 5 Identity (> = v3) bez Entity Framework dla MongoDB.Driver (> = v2.1.0) github.com/saan800/SaanSoft. AspNet.Identity3.MongoDB
Stanislav Prusac

Odpowiedzi:

178

Użyłbym pojedynczej klasy Context dziedziczącej po IdentityDbContext. W ten sposób kontekst może być świadomy wszelkich relacji między twoimi klasami a IdentityUser i rolami IdentityDbContext. IdentityDbContext ma bardzo niewielki narzut, jest to w zasadzie zwykły DbContext z dwoma zestawami DbSet. Jeden dla użytkowników i jeden dla ról.

Olav Nybø
źródło
52
Dotyczy to pojedynczego projektu MVC5, ale nie jest pożądane, gdy pochodny DbContext jest współużytkowany przez wiele projektów, niektóre nie MVC5, gdzie niektóre nie potrzebują obsługi tożsamości.
Dave
Zagłosowano na tę samą bazę danych w celu łatwiejszej konserwacji i lepszej integralności relacyjnej. Ponieważ jednostka użytkownika i jednostka roli będą łatwo powiązane z innymi obiektami aplikacji.
anIBMer
6
@Dave - Komplikuje partycjonowanie danych użytkownika przy użyciu dwóch różnych kontekstów. Czy dane aplikacji MVC dzielą dane według użytkownika, ale inne aplikacje tego nie robią. Udostępnianie tej samej warstwy danych jest powszechne, ale nie sądzę, że niektóre projekty potrzebują danych podzielonych przez użytkownika, a niektóre nie.
RickAndMSFT
1
Czy ktoś wie o tym, jak wyodrębnić tekst ApplicationDBContext z projektu MVC i włączyć go do istniejącej warstwy danych EF? Połączenie tych dwóch elementów, jak opisano powyżej, wydaje się być właściwym podejściem, ale pracuję nad projektem ograniczonym czasowo. Chcę to zrobić dobrze za pierwszym razem, ale chciałbym wiedzieć o wszystkich problemach, które przede mną leżą ...
Mike Devenney
7
Po szukaniu około godziny ta odpowiedź wskazała mi właściwy kierunek - ale nie byłem pewien, jak ją wdrożyć (dla mnie bardzo dosłownie). Więc jeśli to pomaga komuś innemu, odkryłem, że najprostszym sposobem jest otwarcie IdentityModels.cs i dodanie nowego DbSet w klasie ApplicationDbContext.
SeanOB
45

Istnieje wiele nieporozumień na temat IdentityDbContext , szybkiego wyszukiwania w Stackoverflow, a znajdziesz następujące pytania:
Dlaczego Asp.Net Identity IdentityDbContext jest czarną skrzynką?
Jak mogę zmienić nazwy tabel podczas korzystania z Visual Studio 2013 AspNet Identity?
Scal MyDbContext z IdentityDbContext

Aby odpowiedzieć na wszystkie te pytania, musimy zrozumieć, że IdentityDbContext jest tylko klasą odziedziczoną po DbContext.
Rzućmy okiem na źródło IdentityDbContext :

/// <summary>
/// Base class for the Entity Framework database context used for identity.
/// </summary>
/// <typeparam name="TUser">The type of user objects.</typeparam>
/// <typeparam name="TRole">The type of role objects.</typeparam>
/// <typeparam name="TKey">The type of the primary key for users and roles.</typeparam>
/// <typeparam name="TUserClaim">The type of the user claim object.</typeparam>
/// <typeparam name="TUserRole">The type of the user role object.</typeparam>
/// <typeparam name="TUserLogin">The type of the user login object.</typeparam>
/// <typeparam name="TRoleClaim">The type of the role claim object.</typeparam>
/// <typeparam name="TUserToken">The type of the user token object.</typeparam>
public abstract class IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken> : DbContext
    where TUser : IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin>
    where TRole : IdentityRole<TKey, TUserRole, TRoleClaim>
    where TKey : IEquatable<TKey>
    where TUserClaim : IdentityUserClaim<TKey>
    where TUserRole : IdentityUserRole<TKey>
    where TUserLogin : IdentityUserLogin<TKey>
    where TRoleClaim : IdentityRoleClaim<TKey>
    where TUserToken : IdentityUserToken<TKey>
{
    /// <summary>
    /// Initializes a new instance of <see cref="IdentityDbContext"/>.
    /// </summary>
    /// <param name="options">The options to be used by a <see cref="DbContext"/>.</param>
    public IdentityDbContext(DbContextOptions options) : base(options)
    { }

    /// <summary>
    /// Initializes a new instance of the <see cref="IdentityDbContext" /> class.
    /// </summary>
    protected IdentityDbContext()
    { }

    /// <summary>
    /// Gets or sets the <see cref="DbSet{TEntity}"/> of Users.
    /// </summary>
    public DbSet<TUser> Users { get; set; }

    /// <summary>
    /// Gets or sets the <see cref="DbSet{TEntity}"/> of User claims.
    /// </summary>
    public DbSet<TUserClaim> UserClaims { get; set; }

    /// <summary>
    /// Gets or sets the <see cref="DbSet{TEntity}"/> of User logins.
    /// </summary>
    public DbSet<TUserLogin> UserLogins { get; set; }

    /// <summary>
    /// Gets or sets the <see cref="DbSet{TEntity}"/> of User roles.
    /// </summary>
    public DbSet<TUserRole> UserRoles { get; set; }

    /// <summary>
    /// Gets or sets the <see cref="DbSet{TEntity}"/> of User tokens.
    /// </summary>
    public DbSet<TUserToken> UserTokens { get; set; }

    /// <summary>
    /// Gets or sets the <see cref="DbSet{TEntity}"/> of roles.
    /// </summary>
    public DbSet<TRole> Roles { get; set; }

    /// <summary>
    /// Gets or sets the <see cref="DbSet{TEntity}"/> of role claims.
    /// </summary>
    public DbSet<TRoleClaim> RoleClaims { get; set; }

    /// <summary>
    /// Configures the schema needed for the identity framework.
    /// </summary>
    /// <param name="builder">
    /// The builder being used to construct the model for this context.
    /// </param>
    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<TUser>(b =>
        {
            b.HasKey(u => u.Id);
            b.HasIndex(u => u.NormalizedUserName).HasName("UserNameIndex").IsUnique();
            b.HasIndex(u => u.NormalizedEmail).HasName("EmailIndex");
            b.ToTable("AspNetUsers");
            b.Property(u => u.ConcurrencyStamp).IsConcurrencyToken();

            b.Property(u => u.UserName).HasMaxLength(256);
            b.Property(u => u.NormalizedUserName).HasMaxLength(256);
            b.Property(u => u.Email).HasMaxLength(256);
            b.Property(u => u.NormalizedEmail).HasMaxLength(256);
            b.HasMany(u => u.Claims).WithOne().HasForeignKey(uc => uc.UserId).IsRequired();
            b.HasMany(u => u.Logins).WithOne().HasForeignKey(ul => ul.UserId).IsRequired();
            b.HasMany(u => u.Roles).WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
        });

        builder.Entity<TRole>(b =>
        {
            b.HasKey(r => r.Id);
            b.HasIndex(r => r.NormalizedName).HasName("RoleNameIndex");
            b.ToTable("AspNetRoles");
            b.Property(r => r.ConcurrencyStamp).IsConcurrencyToken();

            b.Property(u => u.Name).HasMaxLength(256);
            b.Property(u => u.NormalizedName).HasMaxLength(256);

            b.HasMany(r => r.Users).WithOne().HasForeignKey(ur => ur.RoleId).IsRequired();
            b.HasMany(r => r.Claims).WithOne().HasForeignKey(rc => rc.RoleId).IsRequired();
        });

        builder.Entity<TUserClaim>(b => 
        {
            b.HasKey(uc => uc.Id);
            b.ToTable("AspNetUserClaims");
        });

        builder.Entity<TRoleClaim>(b => 
        {
            b.HasKey(rc => rc.Id);
            b.ToTable("AspNetRoleClaims");
        });

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

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

        builder.Entity<TUserToken>(b => 
        {
            b.HasKey(l => new { l.UserId, l.LoginProvider, l.Name });
            b.ToTable("AspNetUserTokens");
        });
    }
}


Jeśli chcemy połączyć IdentityDbContext z naszym DbContext, mamy do wyboru dwie opcje:

Pierwsza opcja:
Utwórz DbContext, który dziedziczy z IdentityDbContext i ma dostęp do klas.

   public class ApplicationDbContext 
    : IdentityDbContext
{
    public ApplicationDbContext()
        : base("DefaultConnection")
    {
    }

    static ApplicationDbContext()
    {
        Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
    }

    public static ApplicationDbContext Create()
    {
        return new ApplicationDbContext();
    }

    // Add additional items here as needed
}


Dodatkowe uwagi:

1) Możemy również zmienić domyślne nazwy tabel asp.net Identity za pomocą następującego rozwiązania:

    public class ApplicationDbContext : IdentityDbContext
    {    
        public ApplicationDbContext(): base("DefaultConnection")
        {
        }

        protected override void OnModelCreating(System.Data.Entity.DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<IdentityUser>().ToTable("user");
            modelBuilder.Entity<ApplicationUser>().ToTable("user");

            modelBuilder.Entity<IdentityRole>().ToTable("role");
            modelBuilder.Entity<IdentityUserRole>().ToTable("userrole");
            modelBuilder.Entity<IdentityUserClaim>().ToTable("userclaim");
            modelBuilder.Entity<IdentityUserLogin>().ToTable("userlogin");
        }
    }

2) Ponadto możemy rozszerzyć każdą klasę i dodać dowolną właściwość do klas takich jak „IdentityUser”, „IdentityRole”, ...

    public class ApplicationRole : IdentityRole<string, ApplicationUserRole>
{
    public ApplicationRole() 
    {
        this.Id = Guid.NewGuid().ToString();
    }

    public ApplicationRole(string name)
        : this()
    {
        this.Name = name;
    }

    // Add any custom Role properties/code here
}


// Must be expressed in terms of our custom types:
public class ApplicationDbContext 
    : IdentityDbContext<ApplicationUser, ApplicationRole, 
    string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>
{
    public ApplicationDbContext()
        : base("DefaultConnection")
    {
    }

    static ApplicationDbContext()
    {
        Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
    }

    public static ApplicationDbContext Create()
    {
        return new ApplicationDbContext();
    }

    // Add additional items here as needed
}

Aby zaoszczędzić czas, możemy użyć rozszerzalnego szablonu projektu AspNet Identity 2.0, aby rozszerzyć wszystkie klasy.

Druga opcja:(Niezalecane) W
rzeczywistości nie musimy dziedziczyć po IdentityDbContext, jeśli sami piszemy cały kod.
Więc w zasadzie możemy po prostu odziedziczyć DbContext i zaimplementować naszą dostosowaną wersję „OnModelCreating (ModelBuilder builder)” z kodu źródłowego IdentityDbContext

Arvand
źródło
2
@ mike-devenney Oto Twoja odpowiedź na temat połączenia dwóch warstw kontekstu, mam nadzieję, że to pomoże.
Arvand
1
Dzięki Arvandowi, tęskniłem za tym i dość dziwnie natknąłem się na to 1,5 roku później, ponownie analizując temat. :)
Mike Devenney
9

Jest to późny wpis dla ludzi, ale poniżej znajduje się moja implementacja. Zauważysz również, że wyeliminowałem możliwość zmiany domyślnego typu KLUCZA: szczegóły na ten temat można znaleźć w następujących artykułach:

UWAGI:
Należy zauważyć, że nie można używać Guid'skluczy. Wynika to z faktu, że pod maską są one Structi jako takie nie mają rozpakowywania, co pozwoliłoby na ich konwersję z generycznego<TKey> parametru .

KLASY WYGLĄDAJĄ:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser, CustomRole, string, CustomUserLogin, CustomUserRole, CustomUserClaim>
{
    #region <Constructors>

    public ApplicationDbContext() : base(Settings.ConnectionString.Database.AdministrativeAccess)
    {
    }

    #endregion

    #region <Properties>

    //public DbSet<Case> Case { get; set; }

    #endregion

    #region <Methods>

    #region

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        //modelBuilder.Configurations.Add(new ResourceConfiguration());
        //modelBuilder.Configurations.Add(new OperationsToRolesConfiguration());
    }

    #endregion

    #region

    public static ApplicationDbContext Create()
    {
        return new ApplicationDbContext();
    }

    #endregion

    #endregion
}

    public class ApplicationUser : IdentityUser<string, CustomUserLogin, CustomUserRole, CustomUserClaim>
    {
        #region <Constructors>

        public ApplicationUser()
        {
            Init();
        }

        #endregion

        #region <Properties>

        [Required]
        [StringLength(250)]
        public string FirstName { get; set; }

        [Required]
        [StringLength(250)]
        public string LastName { get; set; }

        #endregion

        #region <Methods>

        #region private

        private void Init()
        {
            Id = Guid.Empty.ToString();
        }

        #endregion

        #region public

        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, string> manager)
        {
            // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);

            // Add custom user claims here

            return userIdentity;
        }

        #endregion

        #endregion
    }

    public class CustomUserStore : UserStore<ApplicationUser, CustomRole, string, CustomUserLogin, CustomUserRole, CustomUserClaim>
    {
        #region <Constructors>

        public CustomUserStore(ApplicationDbContext context) : base(context)
        {
        }

        #endregion
    }

    public class CustomUserRole : IdentityUserRole<string>
    {
    }

    public class CustomUserLogin : IdentityUserLogin<string>
    {
    }

    public class CustomUserClaim : IdentityUserClaim<string> 
    { 
    }

    public class CustomRoleStore : RoleStore<CustomRole, string, CustomUserRole>
    {
        #region <Constructors>

        public CustomRoleStore(ApplicationDbContext context) : base(context)
        {
        } 

        #endregion
    }

    public class CustomRole : IdentityRole<string, CustomUserRole>
    {
        #region <Constructors>

        public CustomRole() { }
        public CustomRole(string name) 
        { 
            Name = name; 
        }

        #endregion
    }
Więzień ZERO
źródło
8

Po przejrzeniu abstrakcji IdentityDbContext przekonasz się, że wygląda ona podobnie do pochodnej DbContext. Najłatwiejszą drogą jest odpowiedź Olava, ale jeśli chcesz mieć większą kontrolę nad tym, co się tworzy, i trochę mniej zależności od pakietów tożsamości, spójrz na moje pytanie i odpowiedź tutaj . Istnieje przykładowy kod, jeśli podążysz za linkiem, ale w podsumowaniu po prostu dodajesz wymagane DbSets do własnej podklasy DbContext.

joelmdev
źródło