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:
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.
Per-entity configuration still wins, which is exactly what you want:
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:
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:
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.
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:
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, 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:
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:
configurationBuilder.Conventions.Add(_ => new CodePropertyLengthConvention());
Other conventions that fit this pattern:
- Table names without the pluralization the
DbSetname implies. - A global
RowVersionshadow property on every aggregate root for 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.
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:
ConfigureConventionsin theDbContext: 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 withApplyConfigurationsFromAssembly: 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.
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.
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.



