From b07a95dd09b07b5ab6d2a9b1f2b1fcfa08c2f0b0 Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Tue, 9 Jan 2018 11:32:40 -0800 Subject: [PATCH] Add Store.MaxLengthForKeys (#1555) * Add Store.MaxLengthForKeys If set this will set max length of things we use for composite keys in UserTokens/Logins Needed for https://github.com/aspnet/templating/issues/62 --- samples/IdentitySample.DefaultUI/Startup.cs | 2 +- samples/IdentitySample.Mvc/appsettings.json | 2 +- src/Core/IdentityOptions.cs | 8 ++ src/Core/StoreOptions.cs | 17 +++++ src/EF/IdentityUserContext.cs | 36 ++++++++- test/EF.Test/DbUtil.cs | 83 +++++++++++++++++++-- test/EF.Test/MaxKeyLengthSchemaTest.cs | 79 ++++++++++++++++++++ test/EF.Test/SqlStoreOnlyUsersTestBase.cs | 60 +++------------ test/EF.Test/SqlStoreTestBase.cs | 63 +++------------- 9 files changed, 239 insertions(+), 111 deletions(-) create mode 100644 src/Core/StoreOptions.cs create mode 100644 test/EF.Test/MaxKeyLengthSchemaTest.cs diff --git a/samples/IdentitySample.DefaultUI/Startup.cs b/samples/IdentitySample.DefaultUI/Startup.cs index c165fd40c2..65c9629e73 100644 --- a/samples/IdentitySample.DefaultUI/Startup.cs +++ b/samples/IdentitySample.DefaultUI/Startup.cs @@ -32,7 +32,7 @@ namespace IdentitySample.DefaultUI options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), x => x.MigrationsAssembly("IdentitySample.DefaultUI"))); - services.AddIdentity() + services.AddIdentity(o => o.Stores.MaxLengthForKeys = 128) .AddEntityFrameworkStores() .AddDefaultUI() .AddDefaultTokenProviders(); diff --git a/samples/IdentitySample.Mvc/appsettings.json b/samples/IdentitySample.Mvc/appsettings.json index b8be7f3159..fa18416de1 100644 --- a/samples/IdentitySample.Mvc/appsettings.json +++ b/samples/IdentitySample.Mvc/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=Interop-Shared-10-10;Trusted_Connection=True;MultipleActiveResultSets=true" + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=IdentitySample-MVC-1-10;Trusted_Connection=True;MultipleActiveResultSets=true" }, "Logging": { "IncludeScopes": false, diff --git a/src/Core/IdentityOptions.cs b/src/Core/IdentityOptions.cs index d5e65c8b5a..c246ef9b61 100644 --- a/src/Core/IdentityOptions.cs +++ b/src/Core/IdentityOptions.cs @@ -55,5 +55,13 @@ namespace Microsoft.AspNetCore.Identity /// The for the identity system. /// public TokenOptions Tokens { get; set; } = new TokenOptions(); + + /// + /// Gets or sets the for the identity system. + /// + /// + /// The for the identity system. + /// + public StoreOptions Stores { get; set; } = new StoreOptions(); } } \ No newline at end of file diff --git a/src/Core/StoreOptions.cs b/src/Core/StoreOptions.cs new file mode 100644 index 0000000000..c8d09f1ba3 --- /dev/null +++ b/src/Core/StoreOptions.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Identity +{ + /// + /// Used for store specific options + /// + public class StoreOptions + { + /// + /// If set to a positive number, the default OnModelCreating will use this value as the max length for any + /// properties used as keys, i.e. UserId, LoginProvider, ProviderKey. + /// + public int MaxLengthForKeys { get; set; } + } +} \ No newline at end of file diff --git a/src/EF/IdentityUserContext.cs b/src/EF/IdentityUserContext.cs index 2142b52fb1..65ccab3f7a 100644 --- a/src/EF/IdentityUserContext.cs +++ b/src/EF/IdentityUserContext.cs @@ -2,7 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; +using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore { @@ -91,6 +96,18 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore /// public DbSet UserTokens { get; set; } + private int GetMaxLengthForKeys() + { + // Need to get the actual application service provider, fallback will cause + // options to not work since IEnumerable don't flow across providers + var options = this.GetService() + .Extensions.OfType() + .FirstOrDefault()?.ApplicationServiceProvider + ?.GetService>() + ?.Value?.Stores; + return options != null ? options.MaxLengthForKeys : 0; + } + /// /// Configures the schema needed for the identity framework. /// @@ -99,6 +116,8 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore /// protected override void OnModelCreating(ModelBuilder builder) { + var maxKeyLength = GetMaxLengthForKeys(); + builder.Entity(b => { b.HasKey(u => u.Id); @@ -112,7 +131,6 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore b.Property(u => u.Email).HasMaxLength(256); b.Property(u => u.NormalizedEmail).HasMaxLength(256); - // Replace with b.HasMany(). b.HasMany().WithOne().HasForeignKey(uc => uc.UserId).IsRequired(); b.HasMany().WithOne().HasForeignKey(ul => ul.UserId).IsRequired(); b.HasMany().WithOne().HasForeignKey(ut => ut.UserId).IsRequired(); @@ -127,12 +145,26 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore builder.Entity(b => { b.HasKey(l => new { l.LoginProvider, l.ProviderKey }); + + if (maxKeyLength > 0) + { + b.Property(l => l.LoginProvider).HasMaxLength(maxKeyLength); + b.Property(l => l.ProviderKey).HasMaxLength(maxKeyLength); + } + b.ToTable("AspNetUserLogins"); }); builder.Entity(b => { - b.HasKey(l => new { l.UserId, l.LoginProvider, l.Name }); + b.HasKey(t => new { t.UserId, t.LoginProvider, t.Name }); + + if (maxKeyLength > 0) + { + b.Property(t => t.LoginProvider).HasMaxLength(maxKeyLength); + b.Property(t => t.Name).HasMaxLength(maxKeyLength); + } + b.ToTable("AspNetUserTokens"); }); } diff --git a/test/EF.Test/DbUtil.cs b/test/EF.Test/DbUtil.cs index 7d6b79df70..915c824dfc 100644 --- a/test/EF.Test/DbUtil.cs +++ b/test/EF.Test/DbUtil.cs @@ -1,19 +1,16 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Http; +using System.Data.SqlClient; +using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Xunit; namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test { public static class DbUtil { - public static IServiceCollection ConfigureDbServices(string connectionString, IServiceCollection services = null) - { - return ConfigureDbServices(connectionString, services); - } - public static IServiceCollection ConfigureDbServices(string connectionString, IServiceCollection services = null) where TContext : DbContext { if (services == null) @@ -21,7 +18,10 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test services = new ServiceCollection(); } services.AddHttpContextAccessor(); - services.AddDbContext(options => options.UseSqlServer(connectionString)); + services.AddDbContext(options => + { + options.UseSqlServer(connectionString); + }); return services; } @@ -30,5 +30,74 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test var serviceProvider = ConfigureDbServices(connectionString).BuildServiceProvider(); return serviceProvider.GetRequiredService(); } + + public static bool VerifyMaxLength(SqlConnection conn, string table, int maxLength, params string[] columns) + { + var count = 0; + using ( + var command = + new SqlCommand("SELECT COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS where TABLE_NAME=@Table", conn)) + { + command.Parameters.Add(new SqlParameter("Table", table)); + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + if (!columns.Contains(reader.GetString(0))) + { + continue; + } + if (reader.GetInt32(1) != maxLength) + { + return false; + } + count++; + } + return count == columns.Length; + } + } + } + + public static bool VerifyColumns(SqlConnection conn, string table, params string[] columns) + { + var count = 0; + using ( + var command = + new SqlCommand("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS where TABLE_NAME=@Table", conn)) + { + command.Parameters.Add(new SqlParameter("Table", table)); + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + count++; + if (!columns.Contains(reader.GetString(0))) + { + return false; + } + } + return count == columns.Length; + } + } + } + + public static void VerifyIndex(SqlConnection conn, string table, string index, bool isUnique = false) + { + using ( + var command = + new SqlCommand( + "SELECT COUNT(*) FROM sys.indexes where NAME=@Index AND object_id = OBJECT_ID(@Table) AND is_unique = @Unique", conn)) + { + command.Parameters.Add(new SqlParameter("Index", index)); + command.Parameters.Add(new SqlParameter("Table", table)); + command.Parameters.Add(new SqlParameter("Unique", isUnique)); + using (var reader = command.ExecuteReader()) + { + Assert.True(reader.Read()); + Assert.True(reader.GetInt32(0) > 0); + } + } + } + } } \ No newline at end of file diff --git a/test/EF.Test/MaxKeyLengthSchemaTest.cs b/test/EF.Test/MaxKeyLengthSchemaTest.cs new file mode 100644 index 0000000000..8825fea1e2 --- /dev/null +++ b/test/EF.Test/MaxKeyLengthSchemaTest.cs @@ -0,0 +1,79 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Data.SqlClient; +using Microsoft.AspNetCore.Builder.Internal; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test +{ + public class MaxKeyLengthSchemaTest : IClassFixture + { + private readonly ApplicationBuilder _builder; + private const string DatabaseName = nameof(MaxKeyLengthSchemaTest); + + public MaxKeyLengthSchemaTest(ScratchDatabaseFixture fixture) + { + var services = new ServiceCollection(); + + services + .AddSingleton(new ConfigurationBuilder().Build()) + .AddDbContext(o => o.UseSqlServer(fixture.ConnectionString)) + .AddIdentity(o => o.Stores.MaxLengthForKeys = 128) + .AddEntityFrameworkStores(); + + services.AddLogging(); + + var provider = services.BuildServiceProvider(); + _builder = new ApplicationBuilder(provider); + + using (var scoped = provider.GetRequiredService().CreateScope()) + using (var db = scoped.ServiceProvider.GetRequiredService()) + { + db.Database.EnsureCreated(); + } + } + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public void EnsureDefaultSchema() + { + var db = _builder.ApplicationServices.GetRequiredService(); + VerifyDefaultSchema(db); + } + + private static void VerifyDefaultSchema(IdentityDbContext dbContext) + { + var sqlConn = dbContext.Database.GetDbConnection(); + + using (var db = new SqlConnection(sqlConn.ConnectionString)) + { + db.Open(); + Assert.True(DbUtil.VerifyColumns(db, "AspNetUsers", "Id", "UserName", "Email", "PasswordHash", "SecurityStamp", + "EmailConfirmed", "PhoneNumber", "PhoneNumberConfirmed", "TwoFactorEnabled", "LockoutEnabled", + "LockoutEnd", "AccessFailedCount", "ConcurrencyStamp", "NormalizedUserName", "NormalizedEmail")); + Assert.True(DbUtil.VerifyColumns(db, "AspNetRoles", "Id", "Name", "NormalizedName", "ConcurrencyStamp")); + Assert.True(DbUtil.VerifyColumns(db, "AspNetUserRoles", "UserId", "RoleId")); + Assert.True(DbUtil.VerifyColumns(db, "AspNetUserClaims", "Id", "UserId", "ClaimType", "ClaimValue")); + Assert.True(DbUtil.VerifyColumns(db, "AspNetUserLogins", "UserId", "ProviderKey", "LoginProvider", "ProviderDisplayName")); + Assert.True(DbUtil.VerifyColumns(db, "AspNetUserTokens", "UserId", "LoginProvider", "Name", "Value")); + + Assert.True(DbUtil.VerifyMaxLength(db, "AspNetUsers", 256, "UserName", "Email", "NormalizedUserName", "NormalizedEmail")); + Assert.True(DbUtil.VerifyMaxLength(db, "AspNetRoles", 256, "Name", "NormalizedName")); + Assert.True(DbUtil.VerifyMaxLength(db, "AspNetUserLogins", 128, "LoginProvider", "ProviderKey")); + Assert.True(DbUtil.VerifyMaxLength(db, "AspNetUserTokens", 128, "LoginProvider", "Name")); + + DbUtil.VerifyIndex(db, "AspNetRoles", "RoleNameIndex", isUnique: true); + DbUtil.VerifyIndex(db, "AspNetUsers", "UserNameIndex", isUnique: true); + DbUtil.VerifyIndex(db, "AspNetUsers", "EmailIndex"); + db.Close(); + } + } + } +} \ No newline at end of file diff --git a/test/EF.Test/SqlStoreOnlyUsersTestBase.cs b/test/EF.Test/SqlStoreOnlyUsersTestBase.cs index 9573721fb6..12aacce182 100644 --- a/test/EF.Test/SqlStoreOnlyUsersTestBase.cs +++ b/test/EF.Test/SqlStoreOnlyUsersTestBase.cs @@ -92,62 +92,24 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test using (var db = new SqlConnection(sqlConn.ConnectionString)) { db.Open(); - Assert.True(VerifyColumns(db, "AspNetUsers", "Id", "UserName", "Email", "PasswordHash", "SecurityStamp", + Assert.True(DbUtil.VerifyColumns(db, "AspNetUsers", "Id", "UserName", "Email", "PasswordHash", "SecurityStamp", "EmailConfirmed", "PhoneNumber", "PhoneNumberConfirmed", "TwoFactorEnabled", "LockoutEnabled", "LockoutEnd", "AccessFailedCount", "ConcurrencyStamp", "NormalizedUserName", "NormalizedEmail")); - Assert.False(VerifyColumns(db, "AspNetRoles", "Id", "Name", "NormalizedName", "ConcurrencyStamp")); - Assert.False(VerifyColumns(db, "AspNetUserRoles", "UserId", "RoleId")); - Assert.True(VerifyColumns(db, "AspNetUserClaims", "Id", "UserId", "ClaimType", "ClaimValue")); - Assert.True(VerifyColumns(db, "AspNetUserLogins", "UserId", "ProviderKey", "LoginProvider", "ProviderDisplayName")); - Assert.True(VerifyColumns(db, "AspNetUserTokens", "UserId", "LoginProvider", "Name", "Value")); + Assert.False(DbUtil.VerifyColumns(db, "AspNetRoles", "Id", "Name", "NormalizedName", "ConcurrencyStamp")); + Assert.False(DbUtil.VerifyColumns(db, "AspNetUserRoles", "UserId", "RoleId")); + Assert.True(DbUtil.VerifyColumns(db, "AspNetUserClaims", "Id", "UserId", "ClaimType", "ClaimValue")); + Assert.True(DbUtil.VerifyColumns(db, "AspNetUserLogins", "UserId", "ProviderKey", "LoginProvider", "ProviderDisplayName")); + Assert.True(DbUtil.VerifyColumns(db, "AspNetUserTokens", "UserId", "LoginProvider", "Name", "Value")); + + DbUtil.VerifyIndex(db, "AspNetUsers", "UserNameIndex", isUnique: true); + DbUtil.VerifyIndex(db, "AspNetUsers", "EmailIndex"); + + DbUtil.VerifyMaxLength(db, "AspNetUsers", 256, "UserName", "Email", "NormalizeUserName", "NormalizeEmail"); - VerifyIndex(db, "AspNetUsers", "UserNameIndex", isUnique: true); - VerifyIndex(db, "AspNetUsers", "EmailIndex"); db.Close(); } } - internal static bool VerifyColumns(SqlConnection conn, string table, params string[] columns) - { - var count = 0; - using ( - var command = - new SqlCommand("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS where TABLE_NAME=@Table", conn)) - { - command.Parameters.Add(new SqlParameter("Table", table)); - using (var reader = command.ExecuteReader()) - { - while (reader.Read()) - { - count++; - if (!columns.Contains(reader.GetString(0))) - { - return false; - } - } - return count == columns.Length; - } - } - } - - internal static void VerifyIndex(SqlConnection conn, string table, string index, bool isUnique = false) - { - using ( - var command = - new SqlCommand( - "SELECT COUNT(*) FROM sys.indexes where NAME=@Index AND object_id = OBJECT_ID(@Table) AND is_unique = @Unique", conn)) - { - command.Parameters.Add(new SqlParameter("Index", index)); - command.Parameters.Add(new SqlParameter("Table", table)); - command.Parameters.Add(new SqlParameter("Unique", isUnique)); - using (var reader = command.ExecuteReader()) - { - Assert.True(reader.Read()); - Assert.True(reader.GetInt32(0) > 0); - } - } - } - [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono)] [OSSkipCondition(OperatingSystems.Linux)] diff --git a/test/EF.Test/SqlStoreTestBase.cs b/test/EF.Test/SqlStoreTestBase.cs index 39db75d3db..258caf98c0 100644 --- a/test/EF.Test/SqlStoreTestBase.cs +++ b/test/EF.Test/SqlStoreTestBase.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Linq.Expressions; using System.Security.Claims; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity.Test; using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; @@ -130,63 +129,25 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test using (var db = new SqlConnection(sqlConn.ConnectionString)) { db.Open(); - Assert.True(VerifyColumns(db, "AspNetUsers", "Id", "UserName", "Email", "PasswordHash", "SecurityStamp", + Assert.True(DbUtil.VerifyColumns(db, "AspNetUsers", "Id", "UserName", "Email", "PasswordHash", "SecurityStamp", "EmailConfirmed", "PhoneNumber", "PhoneNumberConfirmed", "TwoFactorEnabled", "LockoutEnabled", "LockoutEnd", "AccessFailedCount", "ConcurrencyStamp", "NormalizedUserName", "NormalizedEmail")); - Assert.True(VerifyColumns(db, "AspNetRoles", "Id", "Name", "NormalizedName", "ConcurrencyStamp")); - Assert.True(VerifyColumns(db, "AspNetUserRoles", "UserId", "RoleId")); - Assert.True(VerifyColumns(db, "AspNetUserClaims", "Id", "UserId", "ClaimType", "ClaimValue")); - Assert.True(VerifyColumns(db, "AspNetUserLogins", "UserId", "ProviderKey", "LoginProvider", "ProviderDisplayName")); - Assert.True(VerifyColumns(db, "AspNetUserTokens", "UserId", "LoginProvider", "Name", "Value")); + Assert.True(DbUtil.VerifyColumns(db, "AspNetRoles", "Id", "Name", "NormalizedName", "ConcurrencyStamp")); + Assert.True(DbUtil.VerifyColumns(db, "AspNetUserRoles", "UserId", "RoleId")); + Assert.True(DbUtil.VerifyColumns(db, "AspNetUserClaims", "Id", "UserId", "ClaimType", "ClaimValue")); + Assert.True(DbUtil.VerifyColumns(db, "AspNetUserLogins", "UserId", "ProviderKey", "LoginProvider", "ProviderDisplayName")); + Assert.True(DbUtil.VerifyColumns(db, "AspNetUserTokens", "UserId", "LoginProvider", "Name", "Value")); - VerifyIndex(db, "AspNetRoles", "RoleNameIndex", isUnique: true); - VerifyIndex(db, "AspNetUsers", "UserNameIndex", isUnique: true); - VerifyIndex(db, "AspNetUsers", "EmailIndex"); + Assert.True(DbUtil.VerifyMaxLength(db, "AspNetUsers", 256, "UserName", "Email", "NormalizedUserName", "NormalizedEmail")); + Assert.True(DbUtil.VerifyMaxLength(db, "AspNetRoles", 256, "Name", "NormalizedName")); + + DbUtil.VerifyIndex(db, "AspNetRoles", "RoleNameIndex", isUnique: true); + DbUtil.VerifyIndex(db, "AspNetUsers", "UserNameIndex", isUnique: true); + DbUtil.VerifyIndex(db, "AspNetUsers", "EmailIndex"); db.Close(); } } - internal static bool VerifyColumns(SqlConnection conn, string table, params string[] columns) - { - var count = 0; - using ( - var command = - new SqlCommand("SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS where TABLE_NAME=@Table", conn)) - { - command.Parameters.Add(new SqlParameter("Table", table)); - using (var reader = command.ExecuteReader()) - { - while (reader.Read()) - { - count++; - if (!columns.Contains(reader.GetString(0))) - { - return false; - } - } - return count == columns.Length; - } - } - } - - internal static void VerifyIndex(SqlConnection conn, string table, string index, bool isUnique = false) - { - using ( - var command = - new SqlCommand( - "SELECT COUNT(*) FROM sys.indexes where NAME=@Index AND object_id = OBJECT_ID(@Table) AND is_unique = @Unique", conn)) - { - command.Parameters.Add(new SqlParameter("Index", index)); - command.Parameters.Add(new SqlParameter("Table", table)); - command.Parameters.Add(new SqlParameter("Unique", isUnique)); - using (var reader = command.ExecuteReader()) - { - Assert.True(reader.Read()); - Assert.True(reader.GetInt32(0) > 0); - } - } - } - [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono)] [OSSkipCondition(OperatingSystems.Linux)]