Your AI Coding Agents Are Running on Someone Else's Cloud (Sponsored)
Autonomous coding agents are writing real code now β but most run on a vendor's servers, with your source code along for the ride. Coder Agents run on infrastructure you control instead: your cloud VPC, on-prem, or fully air-gapped. Connect models from Anthropic, OpenAI, Google, or your own endpoint, and watch every running agent from a single view.
Your Users Type Sentences. Your Search Box Reads Keywords. (Sponsored)
A user types "waterproof jacket under $150 in size large" into your search box. A normal keyword search matches the words, not the meaning β so it ignores the price, skips the size, and returns a wall of results that aren't what they asked for.
Typesense is an open-source search engine that fixes this with built-in Natural Language Search. You add your LLM API key, and Typesense turns a plain-English query into the right filters, sorting, and ranking before it touches your data β no parsing logic for you to write.
It's typo-tolerant and runs in memory, so results appear as users type. It's the open-source alternative to Algolia, with a smaller learning curve than Elasticsearch, and it ships client libraries for .NET, Python, Go, and more. Self-host it with Docker or run it on Typesense Cloud, and drop it into a Laravel Scout or Django app without heavy setup.
See the Natural Language Search demo on GitHub and try it in your own stack.
Clean Architecture is one of the most popular architectural styles in .NET today.
Clean Architecture aims to separate the application's concerns into distinct layers, promoting high cohesion and low coupling.
In the classic implementation you need to hide EF Core behind a Repository so the Application layer never depends on the ORM.
I followed that advice for years.
But after many production projects, I changed my mind and adopted a more pragmatic approach to implementing Clean Architecture.
In this post, I want to explain why I no longer wrap EF Core in a repository inside Clean Architecture, and why I think most teams should do the same.
We will look at what the original Clean Architecture really says and how a pragmatic approach keeps the same boundaries in the code.
In this post, we will explore:
The Traditional Clean Architecture with a Repository
The Hidden Cost of Repositories in Clean Architecture
Pragmatic Clean Architecture: Use EF Core Directly in Application Use Cases
The Hidden Cost of Repositories in Clean Architecture
I have built many systems with this pattern. Here is what tends to happen as the project grows.
1. A repository is an abstraction over an abstraction.
EF Core's DbContext already implements both the Repository pattern and the Unit of Work pattern.
This is stated in the official DbContext summary. When you put IShipmentRepository on top of it, you are wrapping a wrapper.
When we create a repository over EF Core, we create an abstraction over an abstraction, leading to over-engineered solutions.
Each DbSet<TEntity> in your DbContext represents a collection of entities, just like a typical repository.
It allows you to:
Query data using LINQ
Add, update, and remove entities
Project data to other types
2. Method explosion.
What starts as four methods becomes 20:
csharp
1publicinterfaceIShipmentRepository2{3Task<Shipment?>GetByIdAsync(...);4Task<List<Shipment>>GetByOrderIdAsync(...);5Task<List<Shipment>>GetByCarrierAsync(...);6Task<List<Shipment>>GetActiveAsync(...);7Task<List<Shipment>>GetByStatusAsync(...);8Task<List<Shipment>>GetCreatedBetweenAsync(...);9Task<List<Shipment>>GetWithItemsAsync(...);10// ... many more11}
Every new feature adds a new method.
After a year, no one knows whether the method they need already exists, so they add another one that does almost the same thing.
3. Cross-entity queries have no right place.
What if a feature needs a shipment, its order, plus the customer's address?
Where does that method live? IShipmentRepository? IOrderRepository? A new IShipmentOrderRepository?
4. Includes and projections leak or duplicate.
Some callers need Include(x => x.Items), some do not.
Some want a ShipmentResponse projection, and some want the full entity.
You end up with GetByIdWithItemsAsync, GetByIdSlimAsync, GetByIdForReportAsync β or you load too much data on every call.
5. Mocked-repository tests give false confidence.
Mocking IShipmentRepository is fast, but it does not test how EF Core translates your LINQ-to-SQL queries, whether your Include clauses work, or whether your database uniqueness constraints are enforced.
The exact things that break in production.
6. Slower feature delivery.
Every new use case touches three files: the repository interface, the repository implementation, and the handler.
For a single new query, that is a lot of ceremony.
Now, let's look at a pragmatic approach to using EF Core in Clean Architecture.
Pragmatic Clean Architecture: Use EF Core Directly in Application Use Cases
The classic approach creates a lot of unnecessary code in real projects.
I prefer a more pragmatic approach when implementing architecture in my projects.
I allow my Application use cases to talk to EF Core directly.
This is what changes in the pragmatic version of Clean Architecture:
Domain knows nothing about EF Core. (Same as before.)
Application is allowed to depend on Microsoft.EntityFrameworkCore and on the DbContext. (This is the change.)
Infrastructure still owns the concrete DbContext configuration, migrations, interceptors, and any non-EF concerns.
The Dependency Rule still holds for what matters.
Your business rules: entities, value objects, invariants and state transitions don't depend on any framework.
Your use cases call for the right tool to talk to a database: EF Core.
Here is the same CreateShipmentHandler after the rewrite:
When I share this approach, four objections come up almost every time. Let's go through them honestly.
"1. What if we switch database later?"
This is the most common argument for a repository.
I take it seriously, but in reality:
In 99% of projects, the database never changes in production.
Switching from one relational database to another (SQL Server to PostgreSQL, MySQL to PostgreSQL) keeps almost all of your EF Core code the same. EF Core is the abstraction that already supports multiple databases.
Switching from a relational database to a document database (MongoDB, Cosmos DB) is more than a repository implementation swap. It is a different data model, different consistency rules and different query patterns. You will rewrite the use cases regardless of how you wrap the ORM.
A repository "in case we switch" almost never pays off.
So unless you're actively building a multi-database abstraction layer (which most apps don't need), this reasoning doesn't hold.
"2. Doesn't this break testability?"
It changes how you test, and the change is for the better.
A mocked IShipmentRepository returns whatever you tell it to.
It cannot tell you whether your Include works, whether your Where clause translates to the SQL you expect, or whether your projection avoids loading the whole entity.
Two patterns work well without repositories:
Integration tests with Testcontainers. Run your handlers against a real PostgreSQL or SQL Server in Docker.
You test the actual EF Core query, the real schema and the real indexes.
These tests catch the bugs that mocks miss.
EF Core InMemory provider. Useful for fast tests of pure handler logic when you do not care about SQL translation.
It is not a substitute for integration tests, but it is a fine first layer.
For complex, dynamic, combinable queries, the Specification Pattern is a great alternative to a fat repository.
Each specification is a small class describing a filter and a sort query.
You combine the specifications.
Either approach gives you reuse without coupling every use case to one giant interface.
"3. Doesn't this break the Dependency Rule?"
The Dependency Rule exists to protect your Domain: the part of your code that encodes your business and rarely changes.
EF Core is the persistence library you chose.
If you swap it out, you are doing a major rewrite, no matter what.
The cost of pretending otherwise is extra layers, extra files, and slower delivery, every single day, for a switch that almost never happens.
Pragmatic Clean Architecture draws the line where it actually pays off.
Your Domain stays pure.
Your Application uses EF Core. The boundaries that protect your business rules stay intact.
In other words: keep the spirit of the Dependency Rule, drop the ritual.
I want to be fair.
There are real cases where a repository is the right call:
1. Very Complex Queries That Are Used in Many Places
If you have a query that spans multiple aggregates, involves heavy filtering, sorting, or joins, and is used across many features.
Wrapping it into a repository method can reduce duplication and centralize the logic.
2. Team Conventions or Project Constraints
In some organizations, architectural guidelines strictly require the use of repositories to ensure consistency.
Even if EF Core could be used directly, following the team's agreed-upon conventions might be the pragmatic choice.
3. Cross-Cutting Infrastructure Concerns
Sometimes, you want to decorate repositories with additional behavior, such as caching, logging, or auditing.
While these can also be solved with interceptors or middleware, a repository wrapper might be the simplest approach for your context.
4. External Integrations
If your project queries multiple data sources (e.g., EF Core, an external API, and a legacy database), a repository can act as a facade to unify these sources behind a single abstraction.
5. When Using Dapper
When using Dapper, repositories are essential as they abstract SQL from the rest of your application.
In every other project I have built, dropping the repository made the code smaller, clearer, and faster to evolve.
Pragmatic Clean Architecture keeps the essentials of Clean Architecture and trims the parts that do not pay off. In my experience, this approach gives me way more benefits than drawbacks.
Domain stays free of frameworks. Business rules live in entities and value objects.
Application uses EF Core directly via DbContext. No custom IShipmentRepository, no IUnitOfWork.
Infrastructure owns the DbContext configuration, migrations, and external integrations.
Query reuse comes from IQueryable<T> extension methods or the Specification Pattern.
Tests run against a real database with Testcontainers, instead of mocked repositories.
The result is less code, faster delivery, more reliable tests, and the same architectural boundaries that made Clean Architecture worth using in the first place.
If you have been writing repositories on top of EF Core out of habit, try one project without them.
You will thank me later.
P.S.: As developers are now writing most of the code with AI, it does not matter if AI has to replace the repository implementation or change the code directly in your application use cases (in case you switch a database).
Hope you find this newsletter useful. See you next time.
Whenever you're ready, here's how I can help you:
The .NET Senior Playbook is built to:
Fast-track you from junior or mid-level to senior
Keep you growing as a senior
Help you beat any .NET interview
Covers everything: C#, ASP.NET Core, EF Core, system design β answer each question first, reveal the solution, and a test after every chapter proves it stuck. Finish, and you earn a verifiable certificate for your LinkedIn.