根据最新的Stack Overflow调查,PostgreSQL是目前最受欢迎的数据库。当然,作为一款多功能的ORM框架,EF Core与PostgreSQL能很好地配合使用。不过,要将这两者集成起来,还需要完成几个步骤,过程中也有一些注意事项。在本文中,我们将一起走完这些步骤,并实现几个辅助方法,让未来的集成工作更加简单。
如果你只想使用简化后的PostgreSQL连接方式,可以直接跳到文章末尾的TLDR部分。
首先,让我们搭建一个本地的PostgreSQL实例。下面是一个简单的compose.yml文件,可以实现这一功能:
services:
postgres:
image: postgres
environment:
POSTGRES_DB: playground
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
执行docker compose up -d
命令,让数据库启动并运行后,我们来搭建一个测试项目。我们将使用最基本的Minimal API模板:
dotnet new web
我们还可以让日志输出更美观一些,并移除app.Run()
调用,因为我们实际上并不需要一个运行中的主机:
builder.Logging.AddSimpleConsole(c => c.SingleLine = true); // 新增代码
// 移除的代码 -> app.Run()
最重要的是,让我们连接到数据库。目前,我们只需要一个包——Entity Framework Core的PostgreSQL提供程序:
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
有了这个包,我们就应该能够连接到之前部署的数据库了。但首先,我们需要一个DbContext——让我们声明一个空的DbContext:
public class Db(DbContextOptions<Db> options) : DbContext(options) {
}
使用这个DbContext,我们可以在依赖注入容器中注册我们的数据库:
builder.Services.AddDbContext<Db>((sp, options) =>
{
options.UseNpgsql("Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=playground");
});
最后,让我们通过记录连接结果来测试我们的设置。下面的代码可以实现这一点:
await using var scope = app.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<Db>();
var canConnect = await db.Database.CanConnectAsync();
app.Logger.LogInformation("Can connect to database: {CanConnect}", canConnect);
执行dotnet run
后,控制台应该会打印出Can connect to database: True
。这就是我们的设置——为了方便参考,下面是完整的Program.cs代码:
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddSimpleConsole(c => c.SingleLine = true);
builder.Services.AddDbContext<Db>((sp, options) =>
{
options.UseNpgsql("Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=playground");
});
var app = builder.Build();
await using var scope = app.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<Db>();
var canConnect = await db.Database.CanConnectAsync();
app.Logger.LogInformation("Can connect to database: {CanConnect}", canConnect);
public class Db(DbContextOptions<Db> options) : DbContext(options) {
}
连接数据库已经没问题了,但我们如何执行一些SQL操作呢?让我们在下一节中直接进入这个话题。
要执行一个合适的SQL查询,我们需要一个表。让我们搭建一个简单的表,名字就叫Records(记录):
public class Db(DbContextOptions<Db> options) : DbContext(options) {
public DbSet<Record> Records { get; set; } = null!;
}
public class Record
{
public int Id { get; set; }
public required string Name { get; set; }
}
为了进行实验,我们需要一个全新的数据库,每次启动时都创建表。我们可以这样做:
友情提醒:不要在实际应用程序中使用EnsureDeleted或EnsureCreated。
await db.Database.EnsureDeletedAsync();
await db.Database.EnsureCreatedAsync();
有了架构之后,让我们执行最基本的添加和读取记录操作:
db.Add(new Record { Name = "Test" });
await db.SaveChangesAsync();
var records = await db.Records.ToListAsync();
现在,执行dotnet run
后,我们应该能看到EF Core对数据库执行的命令:
INSERT INTO "Records" ("Name") VALUES (@p0) RETURNING "Id";
SELECT r."Id", r."Name" FROM "Records" AS r
执行查询的过程很顺利,不是吗?但是,查询看起来相当难看,每个表名和列名周围都有引号。让我们在下一节中解决这个问题!
PostgreSQL对蛇形命名法(snake_case)有相当强烈的偏好:如果列名或表名不是蛇形命名的,除非用引号括起来,否则PostgreSQL甚至不会识别它。另一方面,EF Core默认使用帕斯卡命名法(PascalCase)。当然,这种不一致会给PostgreSQL查询的工作带来一些麻烦。让我们通过使用一个命名约定包来解决这个问题:
dotnet add package EFCore.NamingConventions
安装好包后,我们可以更新DbContext的注册,应用蛇形命名法约定。这将确保所有表名和列名都会自动转换:
builder.Services.AddDbContext<Db>((sp, options) =>
{
options.UseNpgsql("Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=playground")
.UseSnakeCaseNamingConvention();
});
更改后,我们的查询将会是这样的:
INSERT INTO records (name) VALUES (@p0) RETURNING id;
SELECT r.id, r.name FROM records AS r
现在我们的查询看起来好多了,是时候解决下一个问题了。如果你和我一样,看到连接字符串硬编码应该已经开始觉得不舒服了。让我们在下一节中解决这个问题。
避免硬编码连接字符串很容易——我们将从配置中读取值:
builder.Services.AddDbContext<Db>((sp, options) =>
{
var connectionString = builder.Configuration.GetConnectionString("Postgres");
options.UseNpgsql(connectionString)
.UseSnakeCaseNamingConvention();
});
显然,我们还需要设置这个值。无论使用什么配置源,我们的代码都不应该有太多变化,但正如我在这篇文章中解释的那样,我用于存储此类值的配置源是launchSettings.json,所以我们需要添加以下行:
"ConnectionStrings:Postgres" : "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=playground"
下面是launchSettings.json大致的样子:
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"applicationUrl": "http://localhost:5267",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ConnectionStrings:Postgres" : "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=playground"
}
}
}
}
现在,执行dotnet run
应该会得到和之前完全相同的结果。现在代码已经不错了,但我们能不能创建一个扩展方法,让PostgreSQL数据库的注册更加顺畅呢?让我们开始吧,将配置解析转移到一种更灵活的方法,避免依赖WebApplicationBuilder。
由于我们的连接字符串很可能是必需的,我们需要一个IConfiguration.GetRequiredValue
方法。幸运的是,有一个包提供了这样的方法:
dotnet add package Confi
现在,我们将从依赖注入容器中解析IConfiguration
,而不是依赖WebApplicationBuilder。代码如下:
var config = sp.GetRequiredService<IConfiguration>();
var connectionString = config.GetRequiredValue(configurationPath);
使用这种方法,我们应该能够组装:
using Confi;
// ...
builder.Services.AddPostgreDbContext<Db>();
// ...
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddPostgreDbContext<TContext>(this IServiceCollection services, string configurationPath = "ConnectionStrings:Postgres")
where TContext : DbContext
{
return services.AddDbContext<TContext>((sp, options) => {
var config = sp.GetRequiredService<IConfiguration>();
var connectionString = config.GetRequiredValue(configurationPath);
options.UseNpgsql(connectionString)
.UseSnakeCaseNamingConvention();
});
}
}
同样,结果应该和之前完全相同,但注册只需要一行简单的代码。在最后一节中,你会找到文章的快速回顾,以及一些额外内容,让PostgreSQL集成更加容易。
另外,也欢迎你自己进行实验。你可以在GitHub上找到本文的完整代码。
在本文中,我们实现了一个辅助方法,用于在.NET应用程序中无缝注册PostgreSQL数据库。你不必从头重新创建这个方法,可以使用Persic.EF包:
dotnet add package Persic.EF.Postgres
安装好包后,你只需一行代码就能附加数据库:
builder.Services.AddPostgres<Db>();
当然,你需要先部署数据库。下面是一个简单的compose.yml文件:
services:
postgres:
image: postgres
environment:
POSTGRES_DB: playground
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
最后,不要忘记将连接字符串设置为ConnectionStrings:Postgres
配置值。我的建议是利用launchSettings.json来设置:
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"applicationUrl": "http://localhost:5267",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ConnectionStrings:Postgres" : "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=playground"
}
}
}
}
下面是一个测试连接的简单代码片段:
await using var scope = app.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<Db>();
var canConnect = await db.Database.CanConnectAsync();
app.Logger.LogInformation("Can connect to database: {CanConnect}", canConnect);
我们的PostgreSQL集成之旅到此结束。本文以及Persic.EF.Postgres包都是persic项目的一部分,该项目包含各种与数据库相关的工具。