# Custom Model Conventions in EF Core

> Fifty entity configurations all setting the same string length and decimal precision is not configuration, it is copy-paste. EF Core lets you define model-wide conventions once with ConfigureConventions, and write your own convention classes for anything it does not cover.

Published: 2026-08-28. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/ef-core-custom-conventions

Conventions in EF Core are the default rules the model builder applies to every entity and property, like treating a property named `Id` as the primary key.
You add your own by overriding `ConfigureConventions` in the `DbContext`, which sets type-based defaults such as string length, decimal precision, and value converters.
For rules that depend on names or model structure, write a convention class like `IModelFinalizingConvention`.

Open any mature EF Core codebase and count how many times `HasMaxLength(200)` appears.
Then `HasPrecision(18, 2)`.
Then the same `DateTime` UTC converter, pasted into every configuration class that touches a timestamp.

Every one of those lines is a default pretending to be a decision.
And every new entity is a chance to forget one, which is how you end up with an unbounded `nvarchar(max)` column holding a two-letter country code.

EF Core has a proper answer: conventions.
Set the default once, override it only where the entity genuinely differs.

## ConfigureConventions: Defaults by Type

Override `ConfigureConventions` in your `DbContext`.
It runs before the model is built, and everything it sets acts as a default that individual configurations can still override:

```csharp
public class AppDbContext(DbContextOptions<AppDbContext> options)
    : DbContext(options)
{
    protected override void ConfigureConventions(
        ModelConfigurationBuilder configurationBuilder)
    {
        configurationBuilder.Properties<string>()
            .HaveMaxLength(500);

        configurationBuilder.Properties<decimal>()
            .HavePrecision(18, 2);

        configurationBuilder.Properties<Enum>()
            .HaveConversion<string>()
            .HaveMaxLength(50);
    }
}
```

Three calls, and the entire model now has:

- No accidental `nvarchar(max)` columns. Every string is capped at 500 unless someone consciously raises it.
- Consistent money precision. No more silent truncation because one configuration said `(18, 2)` and another said nothing.
- Every enum stored as a readable string, a choice I unpack in [**mapping enums in EF Core**](https://milanjovanovic.tech/blog/ef-core-enum-mapping).

Per-entity configuration still wins, which is exactly what you want:

```csharp
builder.Property(p => p.Description).HasMaxLength(4000);
```

The default handles the 90 percent case; the override documents the exception.

## Model-Wide Value Converters

A broadly useful convention is a type-wide value converter.
The classic example is enforcing UTC for every `DateTime`, which also addresses the [**Npgsql timestamptz Kind error**](https://milanjovanovic.tech/blog/ef-core-postgresql-datetime-utc-error):

```csharp
public class UtcDateTimeConverter : ValueConverter<DateTime, DateTime>
{
    public UtcDateTimeConverter()
        : base(
            v => v.Kind == DateTimeKind.Utc ? v : v.ToUniversalTime(),
            v => DateTime.SpecifyKind(v, DateTimeKind.Utc))
    {
    }
}
```

Registered once for the whole model:

```csharp
protected override void ConfigureConventions(
    ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<DateTime>()
        .HaveConversion<UtcDateTimeConverter>();
}
```

The same mechanism registers converters for strongly typed IDs and value objects.
If you wrap identifiers in types like `OrderId`, one `Properties<OrderId>().HaveConversion<OrderIdConverter>()` beats fifty per-property calls.
I covered the converter side of this in [**value conversions in EF Core**](https://milanjovanovic.tech/blog/value-conversions-ef-core).

## Can You Remove a Built-In Convention?

Conventions are not just additive.
`configurationBuilder.Conventions` exposes the full convention set, and you can remove the ones you disagree with.

The one I remove most often is cascade delete for required relationships:

```csharp
protected override void ConfigureConventions(
    ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(CascadeDeleteConvention));

    // Only exists (and is only needed) on the SQL Server provider
    configurationBuilder.Conventions.Remove(typeof(SqlServerOnDeleteConvention));
}
```

With those gone, deletes fail loudly instead of fanning out silently, and every cascade in the schema is one someone chose.
Whether you want that depends on how you feel about the tradeoffs in [**cascade delete in EF Core**](https://milanjovanovic.tech/blog/ef-core-cascade-delete), but the point stands: the built-in defaults are opinions, and you are allowed to override them.

## Custom Convention Classes for Everything Else

`ConfigureConventions` handles type-based defaults.
For rules that depend on names, attributes, or model structure, EF Core 7+ lets you write real convention classes.

`IModelFinalizingConvention` is the workhorse: it runs once, right before the model is finalized, with the whole model available for inspection and mutation.
Here is one that caps any string property whose name ends in `Code` at 20 characters:

```csharp
public class CodePropertyLengthConvention : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(
        IConventionModelBuilder modelBuilder,
        IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
        {
            foreach (var property in entityType.GetDeclaredProperties())
            {
                if (property.ClrType == typeof(string) &&
                    property.Name.EndsWith("Code", StringComparison.Ordinal))
                {
                    property.Builder.HasMaxLength(20);
                }
            }
        }
    }
}
```

Register it in the same `ConfigureConventions` override:

```csharp
configurationBuilder.Conventions.Add(_ => new CodePropertyLengthConvention());
```

Other conventions that fit this pattern:

- Table names without the pluralization the `DbSet` name implies.
- A global `RowVersion` shadow property on every aggregate root for [**optimistic locking**](https://milanjovanovic.tech/blog/solving-race-conditions-with-ef-core-optimistic-locking).
- Snake_case column naming for PostgreSQL across the entire model.

One warning: `property.Builder` calls inside a convention use the **convention** precedence level.
That is by design.
It means explicit configuration and data annotations still override your convention, keeping the precedence hierarchy intact: explicit Fluent API beats attributes, attributes beat conventions.

![Configuration precedence hierarchy in EF Core: Fluent API overrides data annotations, which override custom conventions, which override built-in conventions](https://milanjovanovic.tech/blogs/articles/ef-core-custom-conventions/configuration-precedence.png)

## Where Conventions Fit in Your Architecture

Conventions do not replace `IEntityTypeConfiguration<T>` classes.
They change what those classes contain.

After adopting conventions, my per-entity configurations shrink to the things that are genuinely per-entity: keys, relationships, indexes, owned types, and the handful of properties that deviate from the defaults.
The configurations become readable because everything in them is a decision, not boilerplate.

A concrete structure that has worked well for me:

- `ConfigureConventions` in the `DbContext`: type defaults (string length, precision, enum storage, UTC dates, strongly typed ID converters).
- One or two convention classes: naming rules and cross-cutting shadow properties.
- `IEntityTypeConfiguration<T>` per aggregate, applied with `ApplyConfigurationsFromAssembly`: structure and exceptions.

This keeps the persistence model boring and predictable, which is exactly what you want when the domain model is where the interesting decisions live.
That separation is a core theme of [**Pragmatic Clean Architecture**](https://milanjovanovic.tech/pragmatic-clean-architecture).

One operational note: conventions apply to the whole model, so introducing one on an existing database is a schema change.
Adding a 500-character default to a model full of `nvarchar(max)` columns produces a large migration.
Review it, and roll it out like any other wide migration, ideally through the practices in [**EF Core migrations best practices**](https://milanjovanovic.tech/blog/ef-core-migrations-best-practices).

## Summary

Repeated configuration is a smell, and EF Core gives you two tools to eliminate it.
`ConfigureConventions` sets type-based defaults (lengths, precision, converters, enum storage) in one place, and convention classes like `IModelFinalizingConvention` handle name-based and structural rules the simple API cannot express.

The payoff is not just fewer lines.
It is that defaults become enforced instead of remembered.
A new entity added six months from now gets capped strings, correct decimal precision, UTC dates, and string enums without its author thinking about any of it, and the per-entity configuration files are left holding only real decisions.

Set the defaults once.
Make every override a visible exception.

## Frequently asked questions

### What are conventions in EF Core?

Conventions are the default rules EF Core applies when building the model, like treating a property named Id as the primary key. You can add your own defaults with ConfigureConventions or full convention classes, and override them per property where needed.

### How do I set a default max length for all strings in EF Core?

Override ConfigureConventions in your DbContext and call configurationBuilder.Properties<string>().HaveMaxLength(n). Every string property in the model gets that length unless an entity configuration overrides it.

### What is the difference between pre-convention configuration and custom conventions?

ConfigureConventions (pre-convention configuration) sets simple type-based defaults like lengths, precision, and value converters. Custom convention classes implement interfaces like IModelFinalizingConvention and can inspect and rewrite the whole model, which handles rules that depend on names, attributes, or relationships.

### Can I remove a built-in EF Core convention?

Yes. In ConfigureConventions call configurationBuilder.Conventions.Remove with the convention type, for example removing CascadeDeleteConvention to stop EF Core from configuring cascading deletes by default.
