Skip to content

Commit

Permalink
Docs on the new enum/plugin config (#352)
Browse files Browse the repository at this point in the history
Closes #351
  • Loading branch information
roji authored Sep 13, 2024
1 parent 8ee7cf5 commit c12440d
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 59 deletions.
82 changes: 50 additions & 32 deletions conceptual/EFCore.PG/mapping/enum.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,74 @@ By default, any enum properties in your model will be mapped to database integer

However, the Npgsql provider also allows you to map your CLR enums to [database enum types](https://www.postgresql.org/docs/current/static/datatype-enum.html). This option, unique to PostgreSQL, provides the best of both worlds: the enum is internally stored in the database as a number (minimal storage), but is handled like a string (more usable, no need to remember numeric values) and has type safety.

## Creating your database enum
## Setting up your enum with EF

First, you must specify the PostgreSQL enum type on your model, just like you would with tables, sequences or other databases objects:
> [!NOTE]
> Enum mapping has changed considerably in EF 9.0.
If you're using EF 9.0 or above, simply call `MapEnum` inside your `UseNpgsql` invocation.

### [With a connection string](#tab/with-connection-string)

If you're passing a connection string to `UseNpgsql`, simply add the `MapEnum` call as follows:

```c#
protected override void OnModelCreating(ModelBuilder builder)
=> builder.HasPostgresEnum<Mood>();
builder.Services.AddDbContext<MyContext>(options => options.UseNpgsql(
"<connection string>",
o => o.MapEnum<Mood>("mood")));
```

This causes the EF Core provider to create your enum type, `mood`, with two labels: `happy` and `sad`. This will cause the appropriate migration to be created.
This configures all aspects of Npgsql to use your `Mood` enum - both at the EF and the lower-level Npgsql layer - and ensures that the enum is created in the database in EF migrations.

### [With an external NpgsqlDataSource](#tab/with-datasource)

If you are using `context.Database.Migrate()` to create your enums, you need to instruct Npgsql to reload all types after applying your migrations:
If you're creating an external NpgsqlDataSource and passing it to `UseNpgsql`, you must make sure to map your enum on that data independently of the EF-level setup:

```c#
await context.Database.MigrateAsync(token);
var dataSourceBuilder = new NpgsqlDataSourceBuilder("<connection string>");
dataSourceBuilder.MapEnum<Mood>();
var dataSource = dataSourceBuilder.Build();

if (context.Database.GetDbConnection() is NpgsqlConnection npgsqlConnection)
{
await npgsqlConnection.OpenAsync(token);
try
{
await npgsqlConnection.ReloadTypesAsync();
}
finally
{
await npgsqlConnection.CloseAsync();
}
}
builder.Services.AddDbContext<MyContext>(options => options.UseNpgsql(
dataSource,
o => o.MapEnum<Mood>("mood")));
```

***

### Older EF versions

On versions of EF prior to 9.0, enum setup is more involved and consists of several steps; enum mapping has to be done at the lower-level Npgsql layer, and also requires explicit configuration in the EF model for creation in the database via migrations.

#### Creating your database enum

First, you must specify the PostgreSQL enum type on your model, just like you would with tables, sequences or other databases objects:

```c#
protected override void OnModelCreating(ModelBuilder builder)
=> builder.HasPostgresEnum<Mood>();
```

## Mapping your enum
This causes the EF Core provider to create your enum type, `mood`, with two labels: `happy` and `sad`. This will cause the appropriate migration to be created.

#### Mapping your enum

Even if your database enum is created, Npgsql has to know about it, and especially about your CLR enum type that should be mapped to it:

### [NpgsqlDataSource](#tab/with-datasource)
##### [NpgsqlDataSource](#tab/with-datasource)

Since version 7.0, NpgsqlDataSource is the recommended way to use Npgsql. When using NpgsqlDataSource, map your enum when building your data source:

```c#
// Call UseNodaTime() when building your data source:
// Call MapEnum() when building your data source:
var dataSourceBuilder = new NpgsqlDataSourceBuilder(/* connection string */);
dataSourceBuilder.MapEnum<Mood>();
var dataSource = dataSourceBuilder.Build();

builder.Services.AddDbContext<MyContext>(options => options.UseNpgsql(dataSource));
```

### [Without NpgsqlDatasource](#tab/without-datasource)
##### [Without NpgsqlDatasource](#tab/without-datasource)

Since version 7.0, NpgsqlDataSource is the recommended way to use Npgsql. However, if you're not yet using NpgsqlDataSource, map enums by adding the following code, *before* any EF Core operations take place. An appropriate place for this is in the static constructor on your DbContext class:

Expand All @@ -60,18 +80,16 @@ static MyDbContext()
=> NpgsqlConnection.GlobalTypeMapper.MapEnum<Mood>();
```

> [!NOTE]
> If you have multiple context types, all `MapEnum` invocations must be done before *any* of them is used; this means that the code cannot be in your static constructors, but must be moved to the program start.
***

This code lets Npgsql know that your CLR enum type, `Mood`, should be mapped to a database enum called `mood`. Note that if your enum is in a custom schema (not `public`), you must specify that schema in the call to `MapEnum`.

If you're curious as to inner workings, this code maps the enum with the ADO.NET provider - [see here for the full docs](http://www.npgsql.org/doc/types/enums_and_composites.html). When the Npgsql EF Core first initializes, it calls into the ADO.NET provider to get all mapped enums, and sets everything up internally at the EF Core layer as well.

> [!NOTE]
> If you have multiple context types, all `MapEnum` invocations must be done before *any* of them is used; this means that the code cannot be in your static constructors, but must be moved to the program start.
## Using enum properties

Once your enum is mapped and created in the database, you can use your CLR enum type just like any other property:
Once your enum is properly set up with EF, you can use your CLR enum type just like any other property:

```c#
public class Blog
Expand All @@ -96,13 +114,13 @@ using (var ctx = new MyDbContext())
The Npgsql provider only allow adding new values to existing enums, and the appropriate migrations will be automatically created as you add values to your CLR enum type. However, PostgreSQL itself doesn't support removing enum values (since these may be in use), and while renaming values is supported, it isn't automatically done by the provider to avoid using unreliable detection heuristics. Renaming an enum value can be done by including [raw SQL](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/managing?tabs=dotnet-core-cli#arbitrary-changes-via-raw-sql) in your migrations as follows:

```c#
migrationBuilder.Sql(@"ALTER TYPE mood RENAME VALUE 'happy' TO 'thrilled';");
migrationBuilder.Sql("ALTER TYPE mood RENAME VALUE 'happy' TO 'thrilled';");
```

As always, test your migrations carefully before running them on production databases.

## Scaffolding from an existing database

If you're creating your model from an existing database, the provider will recognize enums in your database, and scaffold the appropriate `HasPostgresEnum()` lines in your model. However, the scaffolding process has no knowledge of your CLR type, and will therefore skip your enum columns (warnings will be logged). You will have to create the CLR type, add the global mapping and add the properties to your entities.
If you're creating your model from an existing database, the provider will recognize enums in your database, and scaffold the appropriate `HasPostgresEnum()` lines in your model. However, the scaffolding process has no knowledge of your CLR type, and will therefore skip your enum columns (warnings will be logged). You will have to create the CLR type and perform the proper setup as described above.

In the future it may be possible to scaffold the actual enum type (and with it the properties), but this doesn't happen at the moment.
In the future it may be possible to scaffold the actual enum type (and with it the properties), but this isn't supported at the moment.
35 changes: 22 additions & 13 deletions conceptual/EFCore.PG/mapping/nodatime.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,45 @@ Beyond NodaTime's general advantages, some specific advantages NodaTime for Post

## Setup

To set up the NodaTime plugin, add the [Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime nuget](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime) to your project. Then, configure NodaTime as follows:
To set up the NodaTime plugin, add the [Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime nuget](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime) to your project. Then, configure the NodaTime plugin as follows:

### [NpgsqlDataSource](#tab/with-datasource)
### [EF 9.0, with a connection string](#tab/ef9-with-connection-string)

Since version 7.0, NpgsqlDataSource is the recommended way to use Npgsql. When using NpsgqlDataSource, NodaTime currently has to be configured twice - once at the EF level, and once at the underlying ADO.NET level (there are plans to improve this):
If you're passing a connection string to `UseNpgsql`, simply add the `UseNodaTime` call as follows:

```c#
// Call UseNodaTime() when building your data source:
var dataSourceBuilder = new NpgsqlDataSourceBuilder(/* connection string */);
builder.Services.AddDbContext<MyContext>(options => options.UseNpgsql(
"<connection string>",
o => o.UseNodaTime()));
```

This configures all aspects of Npgsql to use the NodaTime plugin - both at the EF and the lower-level Npgsql layer.

### [With an external NpgsqlDataSource](#tab/with-datasource)

If you're creating an external NpgsqlDataSource and passing it to `UseNpgsql`, you must call `UseNodaTime` on your NpgsqlDataSourceBuilder independently of the EF-level setup:

```c#
var dataSourceBuilder = new NpgsqlDataSourceBuilder("<connection string>");
dataSourceBuilder.UseNodaTime();
var dataSource = dataSourceBuilder.Build();

// Then, when configuring EF Core with UseNpgsql(), call UseNodaTime() there as well:
builder.Services.AddDbContext<MyContext>(options =>
options.UseNpgsql(dataSource, o => o.UseNodaTime()));
builder.Services.AddDbContext<MyContext>(options => options.UseNpgsql(
dataSource,
o => o.UseNodaTime()));
```

### [Without NpgsqlDatasource](#tab/without-datasource)

Since version 7.0, NpgsqlDataSource is the recommended way to use Npgsql. However, if you're not yet using NpgsqlDataSource, configure NodaTime as follows:
### [Older EF versions, with a connection string](#tab/legacy-with-connection-string)

```c#
// Configure NodaTime at the ADO.NET level.
// Configure UseNodaTime at the ADO.NET level.
// This code must be placed at the beginning of your application, before any other Npgsql API is called; an appropriate place for this is in the static constructor on your DbContext class:
static MyDbContext()
=> NpgsqlConnection.GlobalTypeMapper.UseNodaTime();

// Then, when configuring EF Core with UseNpgsql(), call UseNodaTime():
builder.Services.AddDbContext<MyContext>(options =>
options.UseNpgsql(/* connection string */, o => o.UseNodaTime()));
options.UseNpgsql("<connection string>", o => o.UseNodaTime()));
```

***
Expand Down
35 changes: 22 additions & 13 deletions conceptual/EFCore.PG/mapping/nts.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,35 @@ Note that the EF Core NetTopologySuite plugin depends on [the Npgsql ADO.NET Net

## Setup

To use the NetTopologySuite plugin, add the [Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite nuget](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite) to your project. Then, configure NetTopologySuite as followed:
To use the NetTopologySuite plugin, add the [Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite nuget](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite) to your project. Then, configure the NetTopologySuite plugin as follows:

### [NpgsqlDataSource](#tab/with-datasource)
### [EF 9.0, with a connection string](#tab/ef9-with-connection-string)

Since version 7.0, NpgsqlDataSource is the recommended way to use Npgsql. When using NpsgqlDataSource, NetTopologySuite currently has to be configured twice - once at the EF level, and once at the underlying ADO.NET level (there are plans to improve this):
If you're passing a connection string to `UseNpgsql`, simply add the `UseNetTopologySuite` call as follows:

```c#
// Call UseNetTopologySuite() when building your data source:
var dataSourceBuilder = new NpgsqlDataSourceBuilder(/* connection string */);
builder.Services.AddDbContext<MyContext>(options => options.UseNpgsql(
"<connection string>",
o => o.UseNetTopologySuite()));
```

This configures all aspects of Npgsql to use the NetTopologySuite plugin - both at the EF and the lower-level Npgsql layer.

### [With an external NpgsqlDataSource](#tab/with-datasource)

If you're creating an external NpgsqlDataSource and passing it to `UseNpgsql`, you must call `UseNetTopologySuite` on your NpgsqlDataSourceBuilder independently of the EF-level setup:

```c#
var dataSourceBuilder = new NpgsqlDataSourceBuilder("<connection string>");
dataSourceBuilder.UseNetTopologySuite();
var dataSource = dataSourceBuilder.Build();

// Then, when configuring EF Core with UseNpgsql(), call UseNetTopologySuite() there as well:
builder.Services.AddDbContext<MyContext>(options =>
options.UseNpgsql(dataSource, o => o.UseNetTopologySuite()));
builder.Services.AddDbContext<MyContext>(options => options.UseNpgsql(
dataSource,
o => o.UseNetTopologySuite()));
```

### [Without NpgsqlDatasource](#tab/without-datasource)

Since version 7.0, NpgsqlDataSource is the recommended way to use Npgsql. However, if you're not yet using NpgsqlDataSource, configure NetTopologySuite as follows:
### [Older EF versions, with a connection string](#tab/legacy-with-connection-string)

```c#
// Configure NetTopologySuite at the ADO.NET level.
Expand All @@ -38,12 +47,12 @@ static MyDbContext()

// Then, when configuring EF Core with UseNpgsql(), call UseNetTopologySuite():
builder.Services.AddDbContext<MyContext>(options =>
options.UseNpgsql(/* connection string */, o => o.UseNetTopologySuite()));
options.UseNpgsql("<connection string>", o => o.UseNetTopologySuite()));
```

***

The above sets up all the necessary EF mappings and operation translators. In addition, to make sure that the PostGIS extension is installed in your database, add the following to your DbContext:
The above sets up all the necessary EF mappings and operation translators. If you're using EF 6.0, you also need to make sure that the PostGIS extension is installed in your database (later versions do this automatically). Add the following to your DbContext:

```c#
protected override void OnModelCreating(ModelBuilder builder)
Expand Down
38 changes: 37 additions & 1 deletion conceptual/EFCore.PG/release-notes/9.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,52 @@

Npgsql.EntityFrameworkCore.PostgreSQL version 9.0 is under development; previews are available on [nuget.org](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL).

## Improved configuration for enums and plugins

Previously, configuration around enums and plugins (NodaTime, NetTopologySuite) was complicated, requiring multiple setup actions at both the EF and the lower-level Npgsql layers. EF 9.0 improves the configuration story, allowing you to configure enums and plugins via a single EF gesture:

```c#
builder.Services.AddDbContext<MyContext>(options => options.UseNpgsql(
"<connection string>",
o => o.MapEnum<Mood>("mood")));
```

This takes care of everything - EF configuration, lower-level Npgsql configuration and even the addition of the enum to the EF model, which ensures that the enum is created in the database in EF migrations.

See the [enum](../mapping/enum.md), [NodaTime](../mapping/nodatime.md) and [NetTopologySuite](../mapping/nts.md) documentation for more details.

## UUIDv7 GUIDs are generated by default

When your entity types have a `Guid` key, EF Core by default generates key values for new entities client-side - in .NET - before inserting those entity types to the database; this can be better for performance in some situations. Before version 9.0, the provider generated random GUIDs (version 4) by calling the .NET [`Guid.NewGuid()`](https://learn.microsoft.com/en-us/dotnet/api/system.guid.newguid?view=net-8.0#system-guid-newguid) function. Unfortunately, random GUIDs aren't ideal for database indexing and can cause performance issues.

Version 9.0 of the provider now generates the recently standardized version 7 GUIDs, which is a sequential GUID type that's more appropriate for database indexes and improves their performance. This new behavior is by default and will take effect simply by upgrading the provider version.
Version 9.0 of the provider now generates the recently standardized version 7 GUIDs, which is a sequential GUID type that's more appropriate for database indexes and improves their performance. This new behavior is on by default and takes effect simply by upgrading the provider version.

See [this post](https://www.cybertec-postgresql.com/en/unexpected-downsides-of-uuid-keys-in-postgresql) for more details and performance numbers on random vs. sequential GUIDs.

Thanks to [@ChrisJollyAU](https://github.com/ChrisJollyAU) and [@Timovzl](https://github.com/Timovzl) for contributing this improvement!

## Breaking changes

### Enum mappings must now be configured at the EF level

Previously, enum configuration involved mapping the enum at the lower-level Npgsql layer (either via `NpgsqlDataSourceBuilder.MapEnum` or via `NpgsqlConnection.GlobalTypeMapper.MapEnum`); the EF provider automatically picked this configuration up for the EF-level setup. Unfortunately, this design created numerous issues and bugs.

As part of the improved enum configuration story in version 9.0 ([see above](#improved-configuration-for-enums-and-plugins)), enums must now be configured at the EF level; although this is a breaking change for existing applications, it usually results in simplified setup code and fixes various bugs and problematic behavior.

If your application calls `UseNpgsql` with a simple connection string (rather than an NpgsqlDataSource), it simply needs to add a `MapEnum` call there:

```c#
builder.Services.AddDbContext<MyContext>(options => options.UseNpgsql(
"<connection string>",
o => o.MapEnum<Mood>("mood")));
```

All other setup code - the `MapEnum` call on `NpgsqlConnection.GlobalTypeMapper` and the `HasPostgresEnum` call in `OnModelCreating` - can be removed.

If your application passes an NpgsqlDataSource to `UseNpgsql`, it also needs to add the `MapEnum` call as above; but the `MapEnum` call on `NpgsqlDataSourceBuilder` must also be kept.

See the [enum documentation](../mapping/enum.md) for more information.

## Contributors

A big thank you to all the following people who contributed to the 9.0 release!
Expand Down

0 comments on commit c12440d

Please sign in to comment.