📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is convention over configuration, and where does .NET use it in ASP.NET Core and EF Core?

Sensible defaults that remove boilerplate: EF Core key and relationship conventions, ASP.NET Core routing and configuration conventions; overriding defaults; risks of hidden magic.

Mid-Level Theory Intermediate aspnet-core convention-over-configuration design-principles ef-core

🎯 Key points

  • Frameworks supply sensible defaults so you only configure what differs
  • EF Core: Id or <Type>Id is the key, navigation properties imply relationships, DbSet name is the table name
  • ASP.NET Core: appsettings.{Environment}.json, ASPNETCORE_ prefixes, controller naming and attribute routing conventions
  • Override with Fluent API, attributes or ConfigureConventions
  • Team conventions (folder layout, naming) reduce decisions and review noise
  • Risk: hidden magic surprises people who do not know the convention

⏱ 30-second answer

Convention over configuration means a framework assumes sensible defaults, so you only write configuration where your case differs. EF Core is the clearest .NET example: a property called Id or OrderId becomes the primary key, navigation properties create relationships and foreign keys, and table names come from DbSet names. ASP.NET Core loads appsettings.{Environment}.json automatically, maps __ in environment variables to : in configuration keys, and finds controllers by naming. It removes boilerplate and makes projects consistent, but conventions are invisible, so I document team conventions, use ConfigureConventions for project-wide rules, and override explicitly where the default is wrong.

🗣 Full answer (senior)

How I would answer this in an interview

"Convention over configuration means a framework assumes sensible defaults, so you only write configuration where your case differs. It came to prominence with Ruby on Rails, and .NET uses it heavily. In EF Core, a property named Id or <Type>Id becomes the primary key, a navigation plus a matching <Navigation>Id property defines a relationship and foreign key, table names come from DbSet names, and if EF can't find a foreign key property it creates a shadow one. In ASP.NET Core, the host loads appsettings.json then appsettings.{Environment}.json, environment variables and command-line arguments in a known order; Payments__BaseUrl maps to Payments:BaseUrl; MVC finds classes ending in Controller and views in Views/{Controller}/{Action}; static files come from wwwroot. The benefit is less boilerplate and consistent projects. The cost is that conventions are invisible: a rename can silently change your schema. So I learn the core conventions, set project-wide rules with EF Core's ConfigureConventions, configure explicitly where stakes are high (legacy schemas, public routes, security), review every generated migration, and write down our own team conventions."


1. Conventions in .NET

Area Convention Override
EF Core keys Id or <Type>Id is the primary key; integer keys are identity/auto-generated HasKey, [Key], ValueGeneratedNever
EF Core relationships Navigation + <Navigation>Id (or <Principal>Id) is the FK; otherwise a shadow FK is created HasOne/WithMany, HasForeignKey
EF Core tables Named after the DbSet property (else the type name) ToTable, [Table]
EF Core strings Unbounded (nvarchar(max) on SQL Server) HasMaxLength, ConfigureConventions
EF Core deletes Required relationships cascade by default OnDelete(DeleteBehavior.Restrict)
Configuration sources appsettings.json → appsettings.{Env}.json → user secrets (Dev) → env vars → command line Add/remove sources
Env var keys __ maps to : (Payments__BaseUrl); ASPNETCORE_/DOTNET_ prefixes for host settings n/a
Environment ASPNETCORE_ENVIRONMENT (default Production) Set explicitly
MVC Controllers end in Controller; views in Views/{Controller}/{Action}.cshtml [Route], attribute routing
Static files wwwroot WebRootPath
Options binding Property names match config keys (case-insensitive) [ConfigurationKeyName]

2. Benefits vs risks

Benefits Risks
Less boilerplate, faster start Behaviour you didn't write and may not know
Consistent projects: anyone can find things Renames change schemas or routes silently
Fewer decisions in review Defaults unsuitable for production (unbounded strings, cascades)
Frameworks optimise for the common path Debugging "magic" needs framework knowledge

3. When to be explicit

Situation Why
Legacy database schemas Names rarely match conventions
Core aggregates and relationships A silent change is very costly
Public HTTP routes Routes are a contract; keep them explicit and versioned
Security-relevant behaviour Don't rely on implicit defaults for auth or data exposure
Production limits (string lengths, precision) Defaults are rarely right

Production example

A developer renamed CustomerId to ClientId on our Order entity but kept the Customer navigation. EF Core's conventions no longer matched ClientId as the foreign key, so the model got a shadow CustomerId foreign key for the relationship plus a brand-new, unrelated ClientId column. Code that read order.ClientId saw zeros, and in staging orders effectively [lost their link] to customers as far as the application logic was concerned. The migration looked innocent unless you read it carefully. I configured the relationship explicitly in an IEntityTypeConfiguration<Order> with HasForeignKey(o => o.ClientId) and regenerated the migration as a column rename. Since then we review every generated migration in pull requests and configure relationships for core aggregates explicitly.

🔍 What the interviewer is checking

Checks framework fluency and awareness of hidden behaviour.

Green flags

  • Gives concrete EF Core and ASP.NET Core conventions.
  • Knows how to override them (Fluent API, ConfigureConventions).
  • Mentions reviewing migrations.
  • Balances productivity with explicitness.

Red flags

  • Cannot name any EF Core conventions.
  • Treats framework behaviour as magic.

📚 Learn in depth

1. Origin

David Heinemeier Hansson popularised "convention over configuration" with Ruby on Rails (2004): decide the common case once in the framework, so applications only declare deviations. ASP.NET MVC (2009) and Entity Framework Code First brought the same philosophy to .NET.

2. How EF Core applies conventions

EF Core builds its model through a pipeline of model-building conventions that run as you add entities and properties: key discovery, relationship discovery, foreign-key discovery (including shadow properties), table naming, cascade behaviour and more. Data annotations and the Fluent API then override what conventions inferred. Precedence (lowest → highest): convention → data annotations → Fluent API.

3. Customising conventions

protected override void ConfigureConventions(ModelConfigurationBuilder builder)   // EF Core 6+
{
    builder.Properties<string>().HaveMaxLength(256);
    builder.Properties<decimal>().HavePrecision(18, 2);
    builder.Properties<DateTimeOffset>().HaveConversion<DateTimeOffsetToBinaryConverter>(); // e.g. for SQLite
}

EF Core 7+ lets you add or remove model-building conventions (builder.Conventions.Remove(typeof(CascadeDeleteConvention)) or add a custom IModelFinalizingConvention), useful for team-wide rules such as snake_case naming or restricting cascades.

4. ASP.NET Core configuration conventions

Precedence (later wins):
appsettings.json
appsettings.{Environment}.json
user secrets (Development only)
environment variables         Payments__BaseUrl  →  Payments:BaseUrl
command-line arguments        --Payments:BaseUrl=...

Knowing this order explains most "why is production using the wrong value?" incidents.

5. Making conventions visible

Practice Catches
Review generated migrations in every PR Shadow keys, unexpected renames/drops, cascades
dbContext.Model.ToDebugString() or dotnet ef dbcontext script What the model actually looks like
Snapshot/approval tests of the model or migration SQL Unintended model changes
Endpoint listing (/endpoints debug page, OpenAPI) Unexpected routes
Logging resolved configuration at startup (without secrets) Wrong source winning

6. Team conventions

Convention over configuration also applies to your own codebase: feature folders, naming (*Handler, *Endpoint), one way to register modules, one error format. Written down and enforced (analysers, architecture tests, templates), they cut decisions and review noise the same way framework conventions do.

7. Try it yourself

  1. Create Customer and Order with a navigation and no configuration; run dotnet ef migrations add Initial and read the keys/FKs.
  2. Rename the FK property (keep the navigation), add another migration, and look for the shadow FK.
  3. Fix it with HasForeignKey and confirm the migration becomes a rename.
  4. Add ConfigureConventions for string length and decimal precision; regenerate and compare column types.
  5. Set Payments__BaseUrl as an environment variable and confirm it overrides appsettings.json.

💻 Code

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

using var db = new ShopDb();
db.Database.EnsureDeleted();
db.Database.EnsureCreated();

db.Customers.Add(new Customer { Name = "Ava", Orders = { new Order { Total = 49.95m } } });
db.SaveChanges();
Console.WriteLine(db.Orders.Single().CustomerId);   // FK set by convention via the navigation

public sealed class Customer
{
    public int Id { get; set; }                          // primary key by convention
    public required string Name { get; set; }            // C# 11+ 'required'
    public List<Order> Orders { get; } = [];             // one-to-many by convention
}

public sealed class Order
{
    public int Id { get; set; }
    public decimal Total { get; set; }
    public int CustomerId { get; set; }                  // FK by convention: <Navigation>Id
    public Customer Customer { get; set; } = null!;
}

// Explicit configuration for a core relationship, so a rename can't silently create a shadow FK.
public sealed class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder) =>
        builder.HasOne(o => o.Customer)
               .WithMany(c => c.Orders)
               .HasForeignKey(o => o.CustomerId)
               .OnDelete(DeleteBehavior.Restrict);
}

public sealed class ShopDb : DbContext
{
    // Set<T>() returns the DbSet; the table name comes from the property name by convention.
    public DbSet<Customer> Customers => Set<Customer>();
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnConfiguring(DbContextOptionsBuilder options) =>
        options.UseSqlite("Data Source=shop.db");

    protected override void OnModelCreating(ModelBuilder modelBuilder) =>
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(ShopDb).Assembly);

    // Project-wide conventions instead of repeating attributes on every property.
    protected override void ConfigureConventions(ModelConfigurationBuilder builder)
    {
        builder.Properties<string>().HaveMaxLength(256);
        builder.Properties<decimal>().HavePrecision(18, 2);
    }
}

Requires Microsoft.EntityFrameworkCore.Sqlite (EF Core 6+ for ConfigureConventions).

⚠ Common mistakes

1. Relying on conventions you don't know

What happens: a property rename silently changes keys, columns or relationships. Why it is wrong: behaviour changes without any code that obviously says so. Do instead: learn the core conventions and read every generated migration.

2. Accepting defaults unsuitable for production

What happens: unbounded string columns, cascade deletes, default decimal precision. Why it is wrong: performance, data-loss and rounding problems. Do instead: project-wide rules in ConfigureConventions; explicit overrides where needed.

3. Fighting conventions everywhere

What happens: every property and route configured explicitly by habit. Why it is wrong: you lose the boilerplate savings and consistency. Do instead: follow defaults unless there's a reason; document exceptions.

4. Undocumented team conventions

What happens: "everyone knows handlers go in feature folders". Why it is wrong: new starters can't follow invisible rules. Do instead: write them down; enforce with analysers, templates or architecture tests.

5. Not reviewing generated migrations

What happens: migrations committed without reading them. Why it is wrong: shadow FKs, drops and renames-as-drop/add slip into production. Do instead: treat migrations as code; review the SQL (dotnet ef migrations script).

6. Relying on implicit routes for public APIs

What happens: routes derived from controller/action names. Why it is wrong: renaming a class changes a public contract. Do instead: explicit, versioned routes for public endpoints.

7. Misunderstanding configuration precedence

What happens: a value set in appsettings.Production.json is ignored. Why it is wrong: an environment variable or other later source wins. Do instead: know the source order; log non-secret effective config at startup.

8. Wrong environment by default

What happens: ASPNETCORE_ENVIRONMENT unset in a test environment. Why it is wrong: the app runs as Production with production settings. Do instead: set the environment explicitly in every deployment.

9. Leaving cascade deletes on core aggregates

What happens: deleting a customer cascades to orders and invoices. Why it is wrong: accidental mass deletion of business records. Do instead: DeleteBehavior.Restrict (or remove the cascade convention) for important relationships.

10. Treating framework behaviour as magic

What happens: "EF just does it" without knowing how. Why it is wrong: debugging and code review become guesswork. Do instead: inspect the model (Model.ToDebugString()), endpoints and effective configuration.

🏛 Senior insight: trade-offs

Conventions trade explicitness for speed. I accept them where the default is right and visible in review (migrations), and I configure explicitly where the stakes are high: legacy schemas, public routes and anything security-relevant.

Area My approach
New EF Core model Conventions + ConfigureConventions + IEntityTypeConfiguration for exceptions
Legacy database Explicit mapping
Public HTTP routes Explicit, versioned routes
Configuration Standard providers and naming, validated on start
Team code layout Written convention, enforced in CI

🔗 Follow-ups the interviewer may ask

  1. When: Candidate warns conventions can surprise people Why: Probes least astonishment
    How to answer

    The principle of least astonishment says code should behave the way a reasonable user expects from its name, signature and context. In C# that means following .NET conventions: a TryParse method returns bool and never throws for bad input, an ...Async method returns a task and accepts a CancellationToken as its last parameter, properties are cheap and have no side effects, and Equals, GetHashCode and == agree. Surprises such as a GetUser that creates a user, or a property that makes a network call, cause bugs because callers rely on the convention rather than reading the implementation.

  2. When: Candidate says conventions remove boilerplate Why: Checks KISS reasoning
    How to answer

    KISS (Keep It Simple) means choosing the simplest design that meets the actual requirements, so the code is easy to read, test and change. The useful distinction is between essential complexity (the problem really is hard, such as tax rules) and accidental complexity (layers, patterns and frameworks we added). I also separate simple (few interleaved concepts) from easy (familiar or quick to write): copying a framework template is easy but may not be simple. In practice it means a plain class before an abstraction, a direct EF Core query before a generic repository, and readable code over clever LINQ.

  3. When: Candidate describes folder and naming conventions Why: Links to separation of concerns
    How to answer

    Separation of Concerns (Dijkstra's term) means each part of a system addresses one concern, so you can reason about and change it without understanding everything else. In an ASP.NET Core app the typical concerns are HTTP handling (routing, model binding, status codes), business rules, persistence, and cross-cutting concerns such as logging, authentication, validation and error handling. Cross-cutting concerns go into middleware, endpoint filters or decorators rather than every handler. Separation can be by technical layer or by feature (vertical slices), and over-separating is its own problem because one change then spans many files.

🛠 Real-world problems: how you handle them

Renamed foreign key changed the schema
Situation
A developer renamed `CustomerId` to `ClientId` on an [order] entity.
Signals
A migration added a shadow foreign key and a new column; a staging run [lost links] between orders and customers.
Investigation
EF Core's relationship conventions no longer matched the renamed property, so it inferred a shadow key.
Resolution
Configured the relationship explicitly with `HasForeignKey(o => o.ClientId)` and regenerated the migration as a column rename.
Prevention
Migrations reviewed in every pull request; relationships for core aggregates configured explicitly.

How to tell it (STAR)

A simple property rename made EF Core's conventions infer a brand-new foreign key, and in staging we lost the links between orders and customers. I configured that relationship explicitly so the migration became a rename. We now review every generated migration in pull requests, so convention surprises are caught before they reach a database.

↩ Usually asked after

More in Additional Principles

Last updated 31 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me