AspNetCore.Identity 使用 PostgreSQL:如何创建结构?

AspNetCore.Identity using PostgreSQL: how to create structure?

我有一个 ASP.NET 核心网络应用程序 AspNetCore.Identity 登录。它使用 SQL 服务器进行用户身份验证。

我需要使用 PostgreSQL 数据库我添加了一些 nuget 包(EF.* 等),更改了连接字符串和代码

 public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
            });
//                options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection")));
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseNpgsql( Configuration.GetConnectionString("DefaultConnection")));

            services.AddDefaultIdentity<IdentityUser>()
                .AddEntityFrameworkStores<ApplicationDbContext>();

            services.AddControllersWithViews()
                .AddNewtonsoftJson();
            services.AddRazorPages();
        }

但是当我 运行 它连接到 PostgreSQL 数据库并尝试创建结构时,我收到错误消息,这是怎么回事?为什么 EF 无法创建结构?如何解决这个问题?

  Executing endpoint 'WebApplication1.Controllers.HomeController.Index (WebA
pplication1)'
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[3]
      Route matched with {action = "Index", controller = "Home", page = "", area
 = ""}. Executing controller action with signature Microsoft.AspNetCore.Mvc.IAct
ionResult Index() on controller WebApplication1.Controllers.HomeController (WebA
pplication1).
info: Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor[1]
      Executing ViewResult, running view Index.
info: Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor[4]
      Executed ViewResult - view Index executed in 302.6169ms.
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[2]
      Executed action WebApplication1.Controllers.HomeController.Index (WebAppli
cation1) in 315.3645ms
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'WebApplication1.Controllers.HomeController.Index (WebAp
plication1)'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 451.9255ms 200 text/html; charset=utf-8
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET https://localhost:5001/Identity/Account/Logi
n
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint '/Account/Login'
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[3]
      Route matched with {page = "/Account/Login", area = "Identity", action = "
", controller = ""}. Executing page /Account/Login
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[101]
      Executing handler method Microsoft.AspNetCore.Identity.UI.V4.Pages.Account
.Internal.LoginModel.OnGetAsync - ModelState is Valid
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[11
]
      AuthenticationScheme: Identity.External signed out.
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[102]
      Executed handler method OnGetAsync, returned result .
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[103]
      Executing an implicit handler method - ModelState is Valid
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[104]
      Executed an implicit handler method, returned result Microsoft.AspNetCore.
Mvc.RazorPages.PageResult.
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[4]
      Executed page /Account/Login in 104.6291ms
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint '/Account/Login'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 157.5038ms 200 text/html; charset=utf-8
info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 POST https://localhost:5001/Identity/Account/Log
in application/x-www-form-urlencoded 264
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint '/Account/Login'
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[3]
      Route matched with {page = "/Account/Login", area = "Identity", action = "
", controller = ""}. Executing page /Account/Login
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[101]
      Executing handler method Microsoft.AspNetCore.Identity.UI.V4.Pages.Account
.Internal.LoginModel.OnPostAsync - ModelState is Valid
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 3.1.0 initialized 'ApplicationDbContext' using provi
der 'Npgsql.EntityFrameworkCore.PostgreSQL' with options: None
fail: Microsoft.EntityFrameworkCore.Database.Command[20102]
      Failed executing DbCommand (182ms) [Parameters=[@__normalizedUserName_0='?
'], CommandType='Text', CommandTimeout='30']
      SELECT a."Id", a."AccessFailedCount", a."ConcurrencyStamp", a."Email", a."
EmailConfirmed", a."LockoutEnabled", a."LockoutEnd", a."NormalizedEmail", a."Nor
malizedUserName", a."PasswordHash", a."PhoneNumber", a."PhoneNumberConfirmed", a
."SecurityStamp", a."TwoFactorEnabled", a."UserName"
      FROM "AspNetUsers" AS a
      WHERE a."NormalizedUserName" = @__normalizedUserName_0
      LIMIT 1
fail: Microsoft.EntityFrameworkCore.Query[10100]
      An exception occurred while iterating over the results of a query for cont
ext type 'WebApplication1.Data.ApplicationDbContext'.
      Npgsql.PostgresException (0x80004005): 42P01: relation "AspNetUsers" does
not exist
         at Npgsql.NpgsqlConnector.<>c__DisplayClass160_0.<<DoReadMessage>g__Rea
dMessageLong|0>d.MoveNext()
      --- End of stack trace from previous location where exception was thrown -
--
         at Npgsql.NpgsqlConnector.<>c__DisplayClass160_0.<<DoReadMessage>g__Rea
dMessageLong|0>d.MoveNext()
      --- End of stack trace from previous location where exception was thrown -
--
         at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsumin
g)
         at Npgsql.NpgsqlCommand.ExecuteReaderAsync(CommandBehavior behavior, Bo
olean async, CancellationToken cancellationToken)
         at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavi
or, CancellationToken cancellationToken)
         at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReade
rAsync(RelationalCommandParameterObject parameterObject, CancellationToken cance
llationToken)
         at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReade
rAsync(RelationalCommandParameterObject parameterObject, CancellationToken cance
llationToken)
         at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReade
rAsync(RelationalCommandParameterObject parameterObject, CancellationToken cance
llationToken)
         at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.As
yncEnumerator.InitializeReaderAsync(DbContext _, Boolean result, CancellationTok
en cancellationToken)
         at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecuti
onStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 v
erifySucceeded, CancellationToken cancellationToken)
         at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.As
yncEnumerator.MoveNextAsync()
        Exception data:
          Severity: ERROR
          SqlState: 42P01
          MessageText: relation "AspNetUsers" does not exist
          Position: 294
          File: parse_relation.c
          Line: 1159
          Routine: parserOpenTable
Npgsql.PostgresException (0x80004005): 42P01: relation "AspNetUsers" does not ex
ist
   at Npgsql.NpgsqlConnector.<>c__DisplayClass160_0.<<DoReadMessage>g__ReadMessa
geLong|0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Npgsql.NpgsqlConnector.<>c__DisplayClass160_0.<<DoReadMessage>g__ReadMessa
geLong|0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming)
   at Npgsql.NpgsqlCommand.ExecuteReaderAsync(CommandBehavior behavior, Boolean
async, CancellationToken cancellationToken)
   at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, Ca
ncellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync
(RelationalCommandParameterObject parameterObject, CancellationToken cancellatio
nToken)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync
(RelationalCommandParameterObject parameterObject, CancellationToken cancellatio
nToken)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync
(RelationalCommandParameterObject parameterObject, CancellationToken cancellatio
nToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.AsyncEnu
merator.InitializeReaderAsync(DbContext _, Boolean result, CancellationToken can
cellationToken)
   at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStra
tegy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifyS
ucceeded, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.AsyncEnu
merator.MoveNextAsync()
  Exception data:
    Severity: ERROR
    SqlState: 42P01
    MessageText: relation "AspNetUsers" does not exist
    Position: 294
    File: parse_relation.c
    Line: 1159
    Routine: parserOpenTable
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[4]
      Executed page /Account/Login in 2362.999ms
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint '/Account/Login'
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (18ms) [Parameters=[], CommandType='Text', CommandTimeo
ut='30']
      SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_name
space n ON n.oid=c.relnamespace WHERE c.relname='__EFMigrationsHistory');
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 3019.116ms 500 text/html; charset=utf-8

您应该 运行 在更改提供程序和连接字符串后迁移数据库,以便 asp.net 核心身份表将在您的 PostgreSQL 数据库中创建:

  1. Add-Migration InitialCreate

  2. Update-database

文档:https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=vs

除了更改提供程序和连接字符串之外,我还需要更进一步。

在我的 ApplicationDbContext class:

首先,我遇到了 Postgresql 的问题,它想强制将双引号添加到未加引号的标识符中,并自动将它们折叠成小写。 PostgreSQL Docs.

我的解决方案是安装 EFCore.NamingConventions 包。然后重写 OnConfiguring,如下所示。

Identity 3.0 显然期望使用 "dbo" 模式。因此,需要通过覆盖 OnModelCreating 来设置默认模式(本例中的 "public",这也是 Postgresql 的默认模式)来指定。

最后,数据库 table 名称需要小写,这由 OnModelCreating 覆盖中的 for 循环处理。

在ApplicationDbContext中class:

public class ApplicationDbContext
            : IdentityDbContext<IdentityUser<int>,IdentityRole<int>,int>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
        .UseSnakeCaseNamingConvention();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // PostgreSQL uses the public schema by default - not dbo.
        modelBuilder.HasDefaultSchema("public");
        base.OnModelCreating(modelBuilder);

        //Rename Identity tables to lowercase
        foreach (var entity in modelBuilder.Model.GetEntityTypes())
        {
            var currentTableName = modelBuilder.Entity(entity.Name).Metadata.GetDefaultTableName();
            modelBuilder.Entity(entity.Name).ToTable(currentTableName.ToLower());
        }
    } 
    public DbSet<Address> Address { get; set; }
}

这样做之后我添加了一个新的迁移: add-migration modifyIdentityForPostgresql

然后更新了数据库模式: update-database

这导致身份 table 被创建。

Screen clip of newly created Identity tables in Postgresql

我的回答与@Steven Tomlinson 提供的答案类似,但在他的解决方案中 ExampleTableName 变成了 exampletablename<string>,我不太喜欢,因为我的目标是 snake_case .

  1. 创建 Helper.cs class
    public class Helper
    {
        public static string ToUnderscoreCase(string str)
        {
            return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString())).ToLower();
        }
    }
  1. 安装EFCore.NamingConventions

  2. 编辑ApplicationDbContext.cs:

    public class ApplicationDbContext
            : IdentityDbContext<IdentityUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder
            .UseSnakeCaseNamingConvention(); // Not even sure if this is used? it 
didn't seem to make any difference

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.HasDefaultSchema("public");
            base.OnModelCreating(modelBuilder);

            foreach (var entity in modelBuilder.Model.GetEntityTypes())
            {
                var currentTableName = modelBuilder.Entity(entity.Name).Metadata.GetDefaultTableName();
                if (currentTableName.Contains("<"))
                {
                    currentTableName = currentTableName.Split('<')[0];
                }
                modelBuilder.Entity(entity.Name).ToTable(Helper.ToUnderscoreCase(currentTableName));
            }
        }
    }