SERVICE · 08 / 08

.NET Core Application Modernization:
Modern runtime, outdated design.

The application runs on .NET 6, 8, or 9, but it was designed as CRUD over tables. Entities are bags of getters and setters, the rules live in controllers, services, and stored procedures, and every update overwrites the state that came before it. The runtime is fine; the design is what costs you.

Book a free 1-hour consultation →

At a glance

Legacy sources: .NET Core 2.x–3.1 · .NET 5–9 · ASP.NET Core + EF Core CRUD · Anemic entities · Logic in controllers, services, stored procedures
Modern targets: Rich domain model (C# .NET 10) · Separate read models · Event-sourced core where warranted · Tests around the rules
Typical timeline: 6 weeks to 6 months depending on scope
Project range: $15,000–$250,000
U.S. delivery: 100% U.S. citizen workforce, U.S.-soil delivery

What to know before you modernize

Modern runtime, outdated design

The shape is familiar. A controller receives a request, maps it onto an entity, calls SaveChanges, and returns. The entity class declares properties and nothing else. Whatever rules exist are checked in the controller, in a service class, in a stored procedure, or in all three, depending on who wrote that feature and when. The database schema is the model: one table per screen, a status column, a handful of boolean flags, and a ModifiedDate. None of this is a runtime problem. ASP.NET Core and EF Core are current, patched, and fast. It is a design problem, and it has a name: the anemic domain model, where the objects that should own the business rules own only data. This service is for applications in that shape. The runtime stays; the core of the design changes.

The symptoms

An application designed as CRUD over tables shows the same symptoms regardless of industry or size. None of them is fatal on its own. Together they are what turns a feature that should take days into one that takes weeks, and they are what the assessment looks for first.

  • Entities with no behavior
    Classes with public getters and setters and no methods. Every rule that concerns them lives somewhere else, so the class cannot protect its own invariants, and nothing stops a caller from putting it into a state the business would never allow.
  • The same rule in three places
    A validation written in the controller, repeated in a service, and enforced again by a stored procedure or a trigger. They drift. One of the three is wrong at any given time, and which one depends on the path the data took.
  • Status columns and boolean flags standing in for a lifecycle
    IsActive, IsApproved, IsCancelled, and a Status string nobody can enumerate. The legal transitions exist only in the heads of the people who wrote the code, and an illegal combination of flags is a valid row.
  • Last write wins on concurrent edits
    Two users open the same record, both save, and the second overwrites the first without either of them knowing. Nothing detects the collision, because the update is a full-row overwrite with no concurrency token.
  • Audit questions the database cannot answer
    Who changed this, when, and why. A ModifiedBy column answers the first two for the most recent change only, and nothing answers the third. The previous state is gone.
  • Reporting queries fighting the transactional schema
    Reports join a dozen normalized tables, run for minutes, and lock the rows the application is trying to write. The reporting need and the transactional need share one schema that was sized for neither.

What it costs

The cost shows up as time. Features take longer because a change to one rule means finding every place the rule is enforced, and there is no single place to look. Compliance questions become archaeology: an auditor asks what a record looked like on a given date, and the answer is assembled from backups, log files, and memory, if it can be assembled at all. Nightly repair jobs appear, scripts that fix rows the application let into an inconsistent state, and they become permanent because nobody knows which code path produced the bad rows. Reports are built on the transactional schema because it is the only schema there is, so they are slow and they answer the questions the tables happen to support rather than the questions the business asks. Each of these costs is paid every month, and none of them is fixed by a runtime upgrade.

Three remedies, in order

The remedies are applied in a fixed order, and most applications stop after the first or the second. First, a rich domain model: the entity that should own a rule gets the rule, as a method with a name from the business, and the controller, the service, and the stored procedure stop enforcing it. The model is put under tests that read like the rules. Second, read models or a reporting database: the queries that were fighting the transactional schema get a schema shaped for the questions they ask, populated from the write side. That separation of reads from writes is all CQRS means here. Third, and only where the business itself speaks in events, an event-sourced core for that one bounded context. It comes last because it is the most expensive to do well, and a well-built CRUD model beats a poorly built event store.

Where event sourcing fits, and where it does not

Event sourcing earns its place when a bounded context has specific needs. Collaboration: many users act on the same records, and a stale read is a business problem. Intent: the business needs to know why something happened, so an OrderCancelled event with a reason is recorded, not a StatusUpdated. Audit: the history of what happened is itself a requirement, not a reconstruction after the fact. Temporal questions: what was true on a given date has to be answered exactly. Where those needs are absent, it does not fit. Reference data, configuration, and supportive records stay CRUD. It is applied inside one bounded context, never as the architecture of the whole system, and it is a separate decision from CQRS: a read model can sit in front of a plain relational write side. None of it requires microservices or the cloud, and none of it is chosen for raw performance.

Where audit trails come from: interceptors, temporal tables, or events →

How the work runs

The work starts with Event Storming, a workshop with the people who run the business and the people who maintain the code. It produces the boundaries: which parts of the application form one bounded context, which rules belong to it, and which parts are reference data that stays as it is. Then one bounded context is rebuilt at a time, inside the existing codebase and the existing solution, behind the same endpoints. Read models are built alongside, populated from the write side, and the old queries are pointed at them one report at a time. Nothing is cut over until it is proven: the new model runs in parallel with the old path, the results are compared, and the old path is removed only after the comparison has held. The application ships throughout. There is no branch that lives for six months and no day on which everything changes.

What you get from Event Storming →

Runtime housekeeping along the way

The runtime still needs attention while the design work is under way. Microsoft’s support policy is simple: even-numbered releases are Long Term Support with three years of patches, odd-numbered releases are Standard Term Support with two. .NET Core 3.1, .NET 5, .NET 6, and .NET 7 are out of support and receive no security fixes. .NET 8 and .NET 9 both reach end of support on November 10, 2026. .NET 10 is the current LTS, supported until November 14, 2028, and the release we move to as part of the work. Entity Framework Core follows the same schedule, so EF Core 6 and 7 are also out of support; the move to the current release is done alongside the runtime upgrade, breaking-changes list first. Package upgrades and deprecated APIs are handled in the same pass, so the result is a modern design on a supported stack.

Start with an assessment

For an application in this shape, the assessment inventories what decides the plan: entities without behavior and what each one is used for; the rules and where each is enforced, per layer, with the duplicates listed; SQL in stored procedures, triggers, and views, and which rules live only there; audit gaps, meaning the questions about history the current schema cannot answer; concurrency handling, or its absence, on every write path; and the test coverage that exists around the rules today. Everything is read from source, schema, and configuration; nothing is run and nothing in production is touched. The result is a written recommendation: which bounded context to rebuild first, whether a read model or a reporting database is enough for the reporting pain, and whether any context warrants event sourcing at all. The fee is quoted after the free 1-hour consultation and creditable toward a subsequent modernization project.

About the Legacy Software Assessment →

Deliverables

  • Domain model with the rules in one place and tests around them
  • Read models or a reporting database sized to the questions asked
  • Event-sourced core for the context that needs history, with projections
  • Migration plan that runs one bounded context at a time
  • Runtime and package upgrade to the current LTS
  • Documentation and knowledge transfer

Common questions

Is my app “legacy” if it runs on .NET 8?

It can be. Legacy is a property of the design, not of the runtime version. An application on .NET 8 whose entities have no behavior, whose rules are spread across controllers and stored procedures, and whose updates overwrite history has the same cost profile as one on .NET Framework 4.x. The runtime is current; the design is what makes it expensive to change.

What is an anemic domain model, in plain terms?

A set of classes that hold data and no behavior. An Order has properties for status, total, and customer, but the rule that an order cannot be cancelled after it ships lives somewhere else, usually in more than one place. The objects that should own the business rules own only fields, so nothing in the code protects the rules from being bypassed.

Do we need event sourcing?

Usually not everywhere, and often not at all. The honest test is whether the business itself speaks in events for one part of the system: it needs to know why something happened or what was true on a given date. Where that is true, event-source that one bounded context. Where it is not, a well-built relational model with a proper audit mechanism is the better answer.

Can we keep our database and EF Core?

Yes, in most cases. The domain model is a change to the code, not to the storage; EF Core maps a rich model with private setters and methods as readily as it maps an anemic one. Read models can live in the same database as separate tables or in a reporting database. Only an event-sourced context needs an event store, and that is one context, not the whole schema.

Does this require microservices or the cloud?

No. A domain model, read models, and an event-sourced core are design decisions inside one application, and they run in a single deployable on the same server the application uses today. Microservices and cloud hosting are separate decisions with their own costs, and neither is a prerequisite for fixing an anemic design.

How do you avoid a big-bang rewrite?

By rebuilding one bounded context at a time inside the existing codebase, behind the same endpoints, with the new model running in parallel with the old path until the results match. Read models are introduced one report at a time. The application keeps shipping, and no single cutover changes everything at once.

How long does it take, and what does it cost?

The bands on this page apply: 6 weeks to 6 months depending on scope, and $15,000–$250,000. One bounded context with a read model sits at the low end; several contexts with an event-sourced core at the high end. The schedule and price are fixed after the Legacy Software Assessment, whose fee is quoted after the free 1-hour consultation and creditable toward a subsequent modernization project.

What about our reporting and BI?

Reporting is usually the first thing that improves. Read models or a reporting database give reports a schema shaped for the questions they ask, populated from the write side, so they stop competing with the application for the transactional tables. BI tools connect to that reporting schema. Where a context is event-sourced, projections feed the same reporting database from the events.

Not sure where to start?

Start with a fixed-scope Legacy Software Assessment: a read-only review of your application’s source, database, configuration, logs, and architecture, ending in findings with evidence and a written recommendation. Quoted after the free 1-hour consultation; creditable toward a subsequent modernization project.

Related

Audit trail in .NET: Interceptors, temporal tables, or events: what each one records.
Event Sourcing & CQRS book: A 452-page guide and a public .NET reference implementation.
Legacy Software Assessment: Get the verified facts before deciding either way.