Skip to content

Commit bec278e

Browse files
authored
Add schema support infrastructure (#46300)
1 parent 171317d commit bec278e

File tree

11 files changed

+570
-0
lines changed

11 files changed

+570
-0
lines changed

src/Identity/EntityFrameworkCore/src/IdentityDbContext.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,60 @@ protected IdentityDbContext() { }
119119
protected override void OnModelCreating(ModelBuilder builder)
120120
{
121121
base.OnModelCreating(builder);
122+
}
123+
124+
/// <summary>
125+
/// Configures the schema needed for the identity framework for schema version 2.0
126+
/// </summary>
127+
/// <param name="builder">
128+
/// The builder being used to construct the model for this context.
129+
/// </param>
130+
internal override void OnModelCreatingVersion2(ModelBuilder builder)
131+
{
132+
base.OnModelCreatingVersion2(builder);
133+
134+
// Current no differences between Version 2 and Version 1
135+
builder.Entity<TUser>(b =>
136+
{
137+
b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
138+
});
139+
140+
builder.Entity<TRole>(b =>
141+
{
142+
b.HasKey(r => r.Id);
143+
b.HasIndex(r => r.NormalizedName).HasDatabaseName("RoleNameIndex").IsUnique();
144+
b.ToTable("AspNetRoles");
145+
b.Property(r => r.ConcurrencyStamp).IsConcurrencyToken();
146+
147+
b.Property(u => u.Name).HasMaxLength(256);
148+
b.Property(u => u.NormalizedName).HasMaxLength(256);
149+
150+
b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.RoleId).IsRequired();
151+
b.HasMany<TRoleClaim>().WithOne().HasForeignKey(rc => rc.RoleId).IsRequired();
152+
});
153+
154+
builder.Entity<TRoleClaim>(b =>
155+
{
156+
b.HasKey(rc => rc.Id);
157+
b.ToTable("AspNetRoleClaims");
158+
});
159+
160+
builder.Entity<TUserRole>(b =>
161+
{
162+
b.HasKey(r => new { r.UserId, r.RoleId });
163+
b.ToTable("AspNetUserRoles");
164+
});
165+
}
166+
167+
/// <summary>
168+
/// Configures the schema needed for the identity framework for schema version 1.0
169+
/// </summary>
170+
/// <param name="builder">
171+
/// The builder being used to construct the model for this context.
172+
/// </param>
173+
internal override void OnModelCreatingVersion1(ModelBuilder builder)
174+
{
175+
base.OnModelCreatingVersion1(builder);
122176
builder.Entity<TUser>(b =>
123177
{
124178
b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();

src/Identity/EntityFrameworkCore/src/IdentityUserContext.cs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ protected IdentityUserContext() { }
9595
/// </summary>
9696
public virtual DbSet<TUserToken> UserTokens { get; set; } = default!;
9797

98+
/// <summary>
99+
/// Gets the schema version used for versioning.
100+
/// </summary>
101+
protected virtual Version SchemaVersion { get => GetStoreOptions()?.SchemaVersion ?? IdentitySchemaVersions.Version1; }
102+
98103
private StoreOptions? GetStoreOptions() => this.GetService<IDbContextOptions>()
99104
.Extensions.OfType<CoreOptionsExtension>()
100105
.FirstOrDefault()?.ApplicationServiceProvider
@@ -114,6 +119,139 @@ public PersonalDataConverter(IPersonalDataProtector protector) : base(s => prote
114119
/// The builder being used to construct the model for this context.
115120
/// </param>
116121
protected override void OnModelCreating(ModelBuilder builder)
122+
{
123+
var version = GetStoreOptions()?.SchemaVersion ?? IdentitySchemaVersions.Version1;
124+
OnModelCreatingVersion(builder, version);
125+
}
126+
127+
/// <summary>
128+
/// Configures the schema needed for the identity framework for a specific schema version.
129+
/// </summary>
130+
/// <param name="builder">
131+
/// The builder being used to construct the model for this context.
132+
/// </param>
133+
/// <param name="schemaVersion">The schema version.</param>
134+
internal virtual void OnModelCreatingVersion(ModelBuilder builder, Version schemaVersion)
135+
{
136+
if (schemaVersion >= IdentitySchemaVersions.Version2)
137+
{
138+
OnModelCreatingVersion2(builder);
139+
}
140+
else
141+
{
142+
OnModelCreatingVersion1(builder);
143+
}
144+
}
145+
146+
/// <summary>
147+
/// Configures the schema needed for the identity framework for schema version 2.0
148+
/// </summary>
149+
/// <param name="builder">
150+
/// The builder being used to construct the model for this context.
151+
/// </param>
152+
internal virtual void OnModelCreatingVersion2(ModelBuilder builder)
153+
{
154+
// Differences from Version 1:
155+
// - maxKeyLength defaults to 128
156+
// - PhoneNumber has a 256 max length
157+
158+
var storeOptions = GetStoreOptions();
159+
var maxKeyLength = storeOptions?.MaxLengthForKeys ?? 0;
160+
if (maxKeyLength == 0)
161+
{
162+
maxKeyLength = 128;
163+
}
164+
var encryptPersonalData = storeOptions?.ProtectPersonalData ?? false;
165+
PersonalDataConverter? converter = null;
166+
167+
builder.Entity<TUser>(b =>
168+
{
169+
b.HasKey(u => u.Id);
170+
b.HasIndex(u => u.NormalizedUserName).HasDatabaseName("UserNameIndex").IsUnique();
171+
b.HasIndex(u => u.NormalizedEmail).HasDatabaseName("EmailIndex");
172+
b.ToTable("AspNetUsers");
173+
b.Property(u => u.ConcurrencyStamp).IsConcurrencyToken();
174+
175+
b.Property(u => u.UserName).HasMaxLength(256);
176+
b.Property(u => u.NormalizedUserName).HasMaxLength(256);
177+
b.Property(u => u.Email).HasMaxLength(256);
178+
b.Property(u => u.NormalizedEmail).HasMaxLength(256);
179+
b.Property(u => u.PhoneNumber).HasMaxLength(256);
180+
181+
if (encryptPersonalData)
182+
{
183+
converter = new PersonalDataConverter(this.GetService<IPersonalDataProtector>());
184+
var personalDataProps = typeof(TUser).GetProperties().Where(
185+
prop => Attribute.IsDefined(prop, typeof(ProtectedPersonalDataAttribute)));
186+
foreach (var p in personalDataProps)
187+
{
188+
if (p.PropertyType != typeof(string))
189+
{
190+
throw new InvalidOperationException(Resources.CanOnlyProtectStrings);
191+
}
192+
b.Property(typeof(string), p.Name).HasConversion(converter);
193+
}
194+
}
195+
196+
b.HasMany<TUserClaim>().WithOne().HasForeignKey(uc => uc.UserId).IsRequired();
197+
b.HasMany<TUserLogin>().WithOne().HasForeignKey(ul => ul.UserId).IsRequired();
198+
b.HasMany<TUserToken>().WithOne().HasForeignKey(ut => ut.UserId).IsRequired();
199+
});
200+
201+
builder.Entity<TUserClaim>(b =>
202+
{
203+
b.HasKey(uc => uc.Id);
204+
b.ToTable("AspNetUserClaims");
205+
});
206+
207+
builder.Entity<TUserLogin>(b =>
208+
{
209+
b.HasKey(l => new { l.LoginProvider, l.ProviderKey });
210+
211+
if (maxKeyLength > 0)
212+
{
213+
b.Property(l => l.LoginProvider).HasMaxLength(maxKeyLength);
214+
b.Property(l => l.ProviderKey).HasMaxLength(maxKeyLength);
215+
}
216+
217+
b.ToTable("AspNetUserLogins");
218+
});
219+
220+
builder.Entity<TUserToken>(b =>
221+
{
222+
b.HasKey(t => new { t.UserId, t.LoginProvider, t.Name });
223+
224+
if (maxKeyLength > 0)
225+
{
226+
b.Property(t => t.LoginProvider).HasMaxLength(maxKeyLength);
227+
b.Property(t => t.Name).HasMaxLength(maxKeyLength);
228+
}
229+
230+
if (encryptPersonalData)
231+
{
232+
var tokenProps = typeof(TUserToken).GetProperties().Where(
233+
prop => Attribute.IsDefined(prop, typeof(ProtectedPersonalDataAttribute)));
234+
foreach (var p in tokenProps)
235+
{
236+
if (p.PropertyType != typeof(string))
237+
{
238+
throw new InvalidOperationException(Resources.CanOnlyProtectStrings);
239+
}
240+
b.Property(typeof(string), p.Name).HasConversion(converter);
241+
}
242+
}
243+
244+
b.ToTable("AspNetUserTokens");
245+
});
246+
}
247+
248+
/// <summary>
249+
/// Configures the schema needed for the identity framework for schema version 1.0
250+
/// </summary>
251+
/// <param name="builder">
252+
/// The builder being used to construct the model for this context.
253+
/// </param>
254+
internal virtual void OnModelCreatingVersion1(ModelBuilder builder)
117255
{
118256
var storeOptions = GetStoreOptions();
119257
var maxKeyLength = storeOptions?.MaxLengthForKeys ?? 0;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
#nullable enable
2+
virtual Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken>.SchemaVersion.get -> System.Version!
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
using Microsoft.Data.Sqlite;
6+
using Microsoft.EntityFrameworkCore;
7+
using Microsoft.EntityFrameworkCore.Diagnostics;
8+
using Microsoft.Extensions.Configuration;
9+
using Microsoft.Extensions.DependencyInjection;
10+
11+
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test;
12+
13+
public class CustomSchemaTest : IClassFixture<ScratchDatabaseFixture>
14+
{
15+
private readonly ApplicationBuilder _builder;
16+
17+
public CustomSchemaTest(ScratchDatabaseFixture fixture)
18+
{
19+
var services = new ServiceCollection();
20+
services
21+
.AddLogging()
22+
.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build())
23+
.AddDbContext<CustomVersionDbContext>(o =>
24+
o.UseSqlite(fixture.Connection)
25+
.ConfigureWarnings(b => b.Log(CoreEventId.ManyServiceProvidersCreatedWarning)))
26+
.AddIdentity<IdentityUser, IdentityRole>(o =>
27+
{
28+
// Versions >= 3 are custom
29+
o.Stores.SchemaVersion = new Version(3, 0);
30+
})
31+
.AddEntityFrameworkStores<CustomVersionDbContext>();
32+
33+
_builder = new ApplicationBuilder(services.BuildServiceProvider());
34+
using var scope = _builder.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope();
35+
var db = scope.ServiceProvider.GetRequiredService<CustomVersionDbContext>();
36+
db.Database.EnsureCreated();
37+
}
38+
39+
[Fact]
40+
public void CanAddCustomColumn()
41+
{
42+
using var scope = _builder.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope();
43+
var db = scope.ServiceProvider.GetRequiredService<CustomVersionDbContext>();
44+
VersionTwoSchemaTest.VerifyVersion2Schema(db);
45+
using var sqlConn = (SqliteConnection)db.Database.GetDbConnection();
46+
sqlConn.Open();
47+
Assert.True(DbUtil.VerifyColumns(sqlConn, "CustomColumns", "Id"));
48+
}
49+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
using Microsoft.Data.Sqlite;
6+
using Microsoft.EntityFrameworkCore;
7+
using Microsoft.EntityFrameworkCore.Diagnostics;
8+
using Microsoft.Extensions.Configuration;
9+
using Microsoft.Extensions.DependencyInjection;
10+
11+
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test;
12+
13+
public class EmptySchemaTest : IClassFixture<ScratchDatabaseFixture>
14+
{
15+
private readonly ApplicationBuilder _builder;
16+
17+
public EmptySchemaTest(ScratchDatabaseFixture fixture)
18+
{
19+
var services = new ServiceCollection();
20+
services
21+
.AddLogging()
22+
.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build())
23+
.AddDbContext<EmptyDbContext>(o =>
24+
o.UseSqlite(fixture.Connection)
25+
.ConfigureWarnings(b => b.Log(CoreEventId.ManyServiceProvidersCreatedWarning)))
26+
.AddIdentity<IdentityUser, IdentityRole>(o =>
27+
{
28+
// Versions >= 10 are empty
29+
o.Stores.SchemaVersion = new Version(11, 0);
30+
})
31+
.AddEntityFrameworkStores<EmptyDbContext>();
32+
33+
_builder = new ApplicationBuilder(services.BuildServiceProvider());
34+
using var scope = _builder.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope();
35+
var db = scope.ServiceProvider.GetRequiredService<EmptyDbContext>();
36+
db.Database.EnsureCreated();
37+
}
38+
39+
[Fact]
40+
public void CanIgnoreEverything()
41+
{
42+
using var scope = _builder.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope();
43+
var db = scope.ServiceProvider.GetRequiredService<EmptyDbContext>();
44+
VerifyEmptySchema(db);
45+
}
46+
47+
private static void VerifyEmptySchema(EmptyDbContext dbContext)
48+
{
49+
using var sqlConn = (SqliteConnection)dbContext.Database.GetDbConnection();
50+
sqlConn.Open();
51+
Assert.True(DbUtil.VerifyColumns(sqlConn, "AspNetUsers"));
52+
Assert.True(DbUtil.VerifyColumns(sqlConn, "AspNetRoles"));
53+
Assert.True(DbUtil.VerifyColumns(sqlConn, "AspNetUserRoles"));
54+
Assert.True(DbUtil.VerifyColumns(sqlConn, "AspNetUserClaims"));
55+
Assert.True(DbUtil.VerifyColumns(sqlConn, "AspNetUserLogins"));
56+
Assert.True(DbUtil.VerifyColumns(sqlConn, "AspNetUserTokens"));
57+
}
58+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
using Microsoft.Data.Sqlite;
6+
using Microsoft.EntityFrameworkCore;
7+
using Microsoft.EntityFrameworkCore.Diagnostics;
8+
using Microsoft.Extensions.Configuration;
9+
using Microsoft.Extensions.DependencyInjection;
10+
11+
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test;
12+
13+
public class VersionOneSchemaTest : IClassFixture<ScratchDatabaseFixture>
14+
{
15+
private readonly ApplicationBuilder _builder;
16+
17+
public VersionOneSchemaTest(ScratchDatabaseFixture fixture)
18+
{
19+
var services = new ServiceCollection();
20+
21+
services
22+
.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build())
23+
.AddDbContext<VersionOneDbContext>(o =>
24+
o.UseSqlite(fixture.Connection)
25+
.ConfigureWarnings(b => b.Log(CoreEventId.ManyServiceProvidersCreatedWarning)))
26+
.AddIdentity<IdentityUser, IdentityRole>(o =>
27+
{
28+
o.Stores.MaxLengthForKeys = 128;
29+
})
30+
.AddEntityFrameworkStores<VersionOneDbContext>();
31+
32+
services.AddLogging();
33+
34+
_builder = new ApplicationBuilder(services.BuildServiceProvider());
35+
36+
using var scope = _builder.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope();
37+
var db = scope.ServiceProvider.GetRequiredService<VersionOneDbContext>();
38+
db.Database.EnsureCreated();
39+
}
40+
41+
[Fact]
42+
public void EnsureDefaultSchema()
43+
{
44+
using var scope = _builder.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope();
45+
var db = scope.ServiceProvider.GetRequiredService<VersionOneDbContext>();
46+
VerifyVersion1Schema(db);
47+
}
48+
49+
private static void VerifyVersion1Schema(VersionOneDbContext dbContext)
50+
{
51+
using var sqlConn = (SqliteConnection)dbContext.Database.GetDbConnection();
52+
sqlConn.Open();
53+
Assert.True(DbUtil.VerifyColumns(sqlConn, "AspNetUsers", "Id", "UserName", "Email", "PasswordHash", "SecurityStamp",
54+
"EmailConfirmed", "PhoneNumber", "PhoneNumberConfirmed", "TwoFactorEnabled", "LockoutEnabled",
55+
"LockoutEnd", "AccessFailedCount", "ConcurrencyStamp", "NormalizedUserName", "NormalizedEmail"));
56+
Assert.True(DbUtil.VerifyColumns(sqlConn, "AspNetRoles", "Id", "Name", "NormalizedName", "ConcurrencyStamp"));
57+
Assert.True(DbUtil.VerifyColumns(sqlConn, "AspNetUserRoles", "UserId", "RoleId"));
58+
Assert.True(DbUtil.VerifyColumns(sqlConn, "AspNetUserClaims", "Id", "UserId", "ClaimType", "ClaimValue"));
59+
Assert.True(DbUtil.VerifyColumns(sqlConn, "AspNetUserLogins", "UserId", "ProviderKey", "LoginProvider", "ProviderDisplayName"));
60+
Assert.True(DbUtil.VerifyColumns(sqlConn, "AspNetUserTokens", "UserId", "LoginProvider", "Name", "Value"));
61+
62+
Assert.True(DbUtil.VerifyMaxLength(dbContext, "AspNetUsers", 256, "UserName", "Email", "NormalizedUserName", "NormalizedEmail"));
63+
Assert.True(DbUtil.VerifyMaxLength(dbContext, "AspNetRoles", 256, "Name", "NormalizedName"));
64+
Assert.True(DbUtil.VerifyMaxLength(dbContext, "AspNetUserLogins", 128, "LoginProvider", "ProviderKey"));
65+
Assert.True(DbUtil.VerifyMaxLength(dbContext, "AspNetUserTokens", 128, "LoginProvider", "Name"));
66+
67+
DbUtil.VerifyIndex(sqlConn, "AspNetRoles", "RoleNameIndex", isUnique: true);
68+
DbUtil.VerifyIndex(sqlConn, "AspNetUsers", "UserNameIndex", isUnique: true);
69+
DbUtil.VerifyIndex(sqlConn, "AspNetUsers", "EmailIndex");
70+
}
71+
}

0 commit comments

Comments
 (0)