Entity Framework 6 to Entity Framework Core migration is not simply a package upgrade. In an enterprise application, it is an architectural modernization that must preserve business behavior while changing data access, dependency injection, configuration, hosting and database integration.
A practical migration also does not require every database operation to move to EF Core. A mature .NET platform can use EF Core for selected entity-oriented workflows, Dapper for measured high-performance SQL, and ADO.NET for bulk, streaming or specialized database operations.
ShippingPortal is a useful real-world example. Its production database layer is centered on Dapper and ADO.NET. We do not introduce EF Core into an existing hot path merely to make the entire system use one ORM when benchmarking shows the lightweight implementation is faster.
- Treat EF6 to EF Core migration as architectural modernization.
- Baseline SQL Server and application performance first.
- Use EF Core selectively for suitable entity-oriented workflows.
- Keep Dapper for measured high-performance SQL paths.
- Keep ADO.NET for bulk, streaming and specialized operations.
- Migrate business capabilities incrementally and benchmark each step.
1. Why Migrate from Entity Framework 6 to EF Core?
EF Core is the modern Entity Framework stack for current .NET applications. A migration can support modern .NET hosting, dependency injection and a cleaner application architecture.
But a stable legacy application should not be migrated simply because a newer framework exists. Start with the business and engineering reason for modernization.
2. Establish a Performance Baseline Before Migration
Record the existing behavior before replacing an EF6 query. For ShippingPortal, useful measurements include shipment lookup time, SQL duration, database round trips, rows returned, memory use and end-to-end carrier processing.
var stopwatch = Stopwatch.StartNew();
var shipment =
await repository.GetShipmentAsync(
shipmentId,
cancellationToken);
stopwatch.Stop();
logger.LogInformation(
"Shipment lookup completed in {ElapsedMs} ms",
stopwatch.ElapsedMilliseconds);
3. Inventory the Existing EF6 and Database Workloads
Before migration, map the current system instead of converting classes one at a time without understanding their role.
- EF6 entities, DbContexts and relationships
- LINQ queries and generated SQL
- Stored procedures and raw SQL
- Transactions and SaveChanges workflows
- Bulk imports and streaming operations
- External APIs and file-processing boundaries
4. ShippingPortal Today: Dapper and ADO.NET Are Deliberate Choices
ShippingPortal already uses lightweight SQL access for performance-sensitive shipping workloads. Dapper provides explicit SQL with lightweight mapping, while ADO.NET can handle lower-level database operations.
ShippingPortal
↓
Application Workflow
↓
Data Access Boundary
├── Dapper
│ └── Shipment / operational SQL
│
└── ADO.NET
└── Bulk / specialized processing
↓
SQL Server
Modernization should preserve these proven paths when measurements show they are already efficient.
5. The Practical Target: EF Core + Dapper + ADO.NET
A modernized enterprise application can legitimately use all three technologies behind a common application architecture.
Modern .NET Application
│
├── EF Core
│ └── Entity-oriented modules
│
├── Dapper
│ └── Measured hot SQL paths
│
└── ADO.NET
└── Bulk / streaming / specialized work
The application service should depend on the business operation, not on the ORM used underneath it.
6. Introduce EF Core Where It Actually Adds Value
A migrated module might use EF Core for entity-oriented administration, configuration or a workflow that benefits from relational mapping and change tracking.
public async Task<CarrierSetting?> GetAsync(
int id,
CancellationToken cancellationToken)
{
return await dbContext.CarrierSettings
.AsNoTracking()
.SingleOrDefaultAsync(
x => x.Id == id,
cancellationToken);
}
This does not mean high-frequency shipment queries must be rewritten if Dapper is measurably faster.
7. Keep Dapper on Measured High-Performance Paths
A shipment lookup often needs only a focused projection. Dapper keeps the SQL visible and avoids introducing ORM behavior where the workload does not benefit from it.
public async Task<ShipmentSummary?> GetShipmentAsync(
int orderId,
CancellationToken cancellationToken)
{
await using var connection =
new SqlConnection(_connectionString);
return await connection
.QuerySingleOrDefaultAsync<ShipmentSummary>(
"""
SELECT
ID,
OrderID,
TrackingNo,
CourierID,
SenderName,
ReceiverName
FROM tblOrders
WHERE ID = @OrderId
""",
new
{
OrderId = orderId
});
}
If this path is already fast, retaining Dapper is a performance decision, not resistance to modernization.
8. Keep ADO.NET for Bulk and Specialized Processing
ADO.NET remains useful when the application needs direct control over SQL, streaming or high-volume operations.
await using var connection =
new SqlConnection(_connectionString);
await connection.OpenAsync(
cancellationToken);
await using var command =
connection.CreateCommand();
command.CommandText = sql;
await using var reader =
await command.ExecuteReaderAsync(
cancellationToken);
There is no architectural requirement to replace a proven specialized operation with EF Core.
9. Rework DbContext Lifetime for ASP.NET Core
When EF Core is introduced into an ASP.NET Core module, dependency injection should manage the DbContext lifecycle.
builder.Services.AddDbContext<ShippingDbContext>(
options =>
options.UseSqlServer(
configuration.GetConnectionString(
"ShippingDb")));
Keep the context lifetime appropriate for the unit of work and do not register a DbContext as a singleton.
10. Rebuild EF6 Entity Mappings Carefully
EF6 and EF Core do not guarantee identical behavior for every model. Review keys, relationships, nullability, column types, precision, indexes and generated SQL.
public sealed class Shipment
{
public int Id { get; set; }
public string TrackingNo { get; set; } =
string.Empty;
public int CourierId { get; set; }
public DateTime CreatedDate { get; set; }
}
11. Re-Test LINQ Queries After Migration
A LINQ query that worked well in EF6 can generate different SQL after migration. Re-test important queries against realistic data and inspect their execution plans.
var shipments =
await dbContext.Shipments
.AsNoTracking()
.Where(x =>
x.UserId == userId &&
x.Status == "Processed")
.OrderByDescending(x => x.Id)
.Take(50)
.Select(x => new ShipmentSummary
{
Id = x.Id,
TrackingNo = x.TrackingNo,
Status = x.Status
})
.ToListAsync(cancellationToken);
12. Preserve Raw SQL Where It Provides Better Control
Migration is not a reason to delete explicit SQL. Complex shipping queries, reports or performance-sensitive reads can remain SQL-driven when the measured workload supports it.
Dapper can provide a lightweight implementation while the rest of the application adopts modern ASP.NET Core architecture.
13. Revalidate SQL Server Indexes After Migration
A change in generated SQL can change how SQL Server uses indexes. Recheck execution plans for critical queries.
CREATE INDEX IX_tblOrders_User_Status
ON dbo.tblOrders
(
UserID,
Status,
ID
);
This is an illustrative candidate, not a blanket index recommendation. Base real index changes on measured workloads.
14. Revalidate Transactions and Concurrency
Transaction boundaries are part of application behavior. During migration, confirm that operations requiring atomic database changes still behave correctly.
Validate shipment
↓
Write related database state
↓
Commit
↓
External carrier operation
↓
Persist carrier result
Do not keep a database transaction open while waiting for DHL, UPS, DPD or Asendia network calls.
15. Benchmark EF Core Against the Existing Implementation
This is the most important rule for a performance-sensitive migration. Compare the proposed EF Core implementation against the existing Dapper or ADO.NET version using the same workload.
Existing Dapper / ADO.NET
↓
Benchmark
Proposed EF Core
↓
Benchmark
Compare:
• response time
• CPU
• logical reads
• allocations
• rows returned
If EF Core is slower on a critical path, keeping the existing implementation is a valid modernization outcome.
16. A Realistic EF6-to-Modern .NET Strategy for ShippingPortal
ShippingPortal should not be migrated by replacing every Dapper query with EF Core. The safer approach is to modernize the application boundary and then move individual workloads when there is a measurable benefit.
Stage 1
Existing ShippingPortal
Dapper + ADO.NET
↓
SQL Server
Stage 2
Modern ASP.NET Core architecture
↓
Application Services / Contracts
↓
Dapper + ADO.NET
Stage 3
Selective EF Core
↓
New or migrated entity workflows
Dapper remains on measured hot paths.
ADO.NET remains for specialized operations.
17. Example: Do Not Convert a Fast Shipment Search Just to Use EF Core
A shipment grid may require filtering, sorting, pagination and only a small projection. If the existing Dapper query is fast, rewriting it with EF Core creates migration work without a guaranteed benefit.
SELECT
ID,
OrderID,
TrackingNo,
CourierID,
SenderName,
ReceiverName
FROM tblOrders
WHERE UserID = @UserId
ORDER BY ID DESC
OFFSET @Offset ROWS
FETCH NEXT @PageSize ROWS ONLY;
Keep this workload on the implementation that performs best, provided it remains maintainable and secure.
18. Keep Carrier API Integration Independent from the ORM
The carrier layer should receive a common shipment model. It should not care whether the model came from EF Core, Dapper or ADO.NET.
Shipment Workflow
↓
Common Shipment Model
↓
Carrier Client
┌───┼────┬────┐
↓ ↓ ↓ ↓
DHL UPS DPD Asendia
↓
Carrier Response
This keeps DHL API Integration, UPS API Integration, DPD API Integration and Asendia API Integration outside the ORM decision.
19. Use Dependency Injection to Hide the Data-Access Choice
public interface IShipmentRepository
{
Task<ShipmentSummary?> GetAsync(
int shipmentId,
CancellationToken cancellationToken);
}
The higher-level application service depends on the contract. The implementation can be EF Core, Dapper or ADO.NET without changing the business workflow.
20. Migrate One Business Capability at a Time
A migration can start with a bounded capability such as administration, configuration, reporting or a new module.
Select capability
↓
Define application boundary
↓
Implement modern version
↓
Functional tests
↓
Performance benchmark
↓
Release
↓
Move next capability
21. Keep Long-Running Work Outside Request Latency
Modernizing a legacy application is also an opportunity to move suitable tracking, synchronization or batch work away from user-facing requests.
API Request
↓
Create Work Item
↓
Background Worker
↓
SQL / Carrier Processing
↓
Persist Result
22. Measure SQL and Carrier Latency Separately
A shipment request can combine SQL Server work and an external carrier call. Measure those boundaries independently.
Shipment Request
↓
SQL Server
↓
Prepare Carrier Request
↓
DHL / UPS / DPD / Asendia
↓
Persist Result
This prevents external network latency from being mistaken for an EF Core or SQL Server problem.
23. Common EF6 to EF Core Migration Mistakes
- Replacing every repository without benchmarking.
- Assuming EF Core generates identical SQL to EF6.
- Removing optimized Dapper queries for uniformity.
- Ignoring SQL Server execution plans after migration.
- Changing transaction or DbContext behavior unintentionally.
- Migrating the entire platform in one release.
24. When EF Core Should Not Replace Dapper or ADO.NET
EF Core is a capable ORM, but it is not automatically the fastest or simplest choice for every database operation. Retain Dapper when explicit SQL and lightweight mapping are measurably better, and retain ADO.NET for bulk, streaming or specialized work.
ShippingPortal is an example of this principle. We do not introduce EF Core into existing hot paths when the measured Dapper or ADO.NET implementation is faster.
EF6 to EF Core Migration Checklist
- Have you documented the existing EF6 entities and queries?
- Have you established a SQL Server and application baseline?
- Have you identified workloads that genuinely benefit from EF Core?
- Have you benchmarked EF Core against existing Dapper paths?
- Have you retained ADO.NET where specialized processing needs it?
- Have you revalidated indexes and generated SQL?
- Have you reviewed DbContext, transaction and concurrency behavior?
- Are carrier integrations independent of the ORM choice?
- Can each capability be migrated and tested independently?
- Have performance results been verified with production-like data?
Conclusion
Entity Framework 6 to Entity Framework Core migration should be treated as a controlled modernization project, not a mechanical replacement of every database operation.
A strong enterprise .NET architecture can combine EF Core, Dapper and ADO.NET behind clear application boundaries. EF Core can serve selected entity-oriented workflows, Dapper can remain on measured high-performance SQL paths and ADO.NET can handle bulk, streaming and specialized operations.
ShippingPortal demonstrates why this approach matters. Its existing Dapper and ADO.NET paths are optimized for performance-sensitive shipping workloads. Modernization should introduce modern architecture and selective EF Core adoption where it creates value, without slowing proven hot paths simply for technology uniformity.