Author: admin

  • Clean Architecture in .NET: Where It Earns Its Complexity

    Clean Architecture in .NET: Where It Earns Its Complexity

    Clean architecture has become close to a default recommendation for new .NET projects, and for good reason: separating domain logic from infrastructure concerns makes a codebase easier to test and easier to change later. It also adds real ceremony: more projects, more interfaces, more indirection between a request coming in and a database row being written. That ceremony is worth it in some situations and genuine overhead in others.

    What Clean Architecture Actually Buys You

    The core promise is that your business rules do not know or care whether they are backed by SQL Server, Cosmos DB, or an in memory list during tests. That independence pays off clearly in a few situations.

    Complex domain logic that changes often. A quality assurance system tracking parts through a multi stage automotive supply chain, with rules that shift as regulations and supplier agreements change, benefits enormously from having that logic isolated in a domain layer that can be tested without spinning up a database.

    Multiple entry points into the same logic. If the same business rules need to run from a web API, a background job, and an internal admin tool, clean architecture stops you from duplicating that logic three times or coupling it awkwardly to one specific host.

    Long project lifespans with changing infrastructure. If you genuinely expect to swap a database technology, or add a second one, in the next few years, the abstraction pays for itself when that day arrives.

    Where It Becomes Overhead

    Small CRUD focused applications. An internal tool that reads and writes fairly simple records does not need four layers of indirection to do it. The extra projects and interfaces add friction without adding testability that matters, because there is not much business logic to isolate in the first place.

    Teams new to the pattern under time pressure. Clean architecture has a real learning curve. A team unfamiliar with it, working against a tight deadline, often ends up with the structure but not the discipline: dependencies leak across layers anyway, and you are left paying the complexity cost without the benefit.

    Prototypes and proof of concepts. If there is a real chance the whole approach gets thrown away after validation, invest the architecture effort later, once you know the product is worth building properly.

    A Practical Middle Ground

    src/

    Domain/ // entities, business rules, no dependencies

    Application/ // use cases, orchestration

    Infrastructure/ // EF Core, external APIs

    Api/ // minimal API endpoints

    For most greenfield .NET projects that are not trivial CRUD, this four folder structure gives you real separation without the heavier ceremony some clean architecture templates default to, such as separate projects per layer with their own dependency graphs. Start with folders inside one project. Split into separate projects only once the codebase is large enough that build times or team boundaries genuinely require it.

    The Decision in One Question

    Before adopting clean architecture on a new build, ask honestly: is the domain logic in this application complex enough, and likely to change often enough, that isolating it from infrastructure will save real time over the life of the project? If yes, the structure earns its keep. If the honest answer is that this is mostly a data entry and reporting tool, a simpler, more direct architecture will get you to production faster and will not cost you anything meaningful in maintainability later.

    Architecture decisions made out of habit, rather than out of an honest read on the specific project, are one of the most common sources of wasted effort on greenfield builds.

  • WebForms to Blazor: A Realistic Migration Path, Not a Rewrite

    WebForms to Blazor: A Realistic Migration Path, Not a Rewrite

    WebForms applications have a reputation problem. They are old, the tooling around them has quietly stopped improving, and every conference talk assumes you have already left. None of that changes the fact that a lot of WebForms applications are still running real logistics and operations businesses today, reliably, and a full rewrite is rarely the responsible next step.

    Here is a more realistic path from WebForms to Blazor, one that respects the fact that the current system works and the team maintaining it has other priorities too.

    Start With What Actually Needs to Change

    Not every screen in a WebForms application needs modernising at the same time. Before writing any Blazor code, map the application by how often each screen is touched and how much business risk it carries. A rarely used admin screen with stable requirements can wait. A dispatch screen that every warehouse operator opens fifty times a day, built on postback patterns that make small changes painful, is where the return on modernisation is highest.

    This mapping exercise alone often reshapes the whole plan. Teams that assume they need to migrate everything usually find that twenty percent of the screens carry eighty percent of the daily pain.

    Run Both Frameworks Side by Side

    You do not need a big bang cutover. IIS can host a WebForms application and a Blazor Server or Blazor WebAssembly application side by side, with routing or a reverse proxy directing traffic to the right one per URL path. This means you can migrate one screen, ship it, get real feedback from the people using it daily, and migrate the next one, rather than committing to a multi month freeze on new features while the whole thing gets rebuilt.

    // Reverse proxy style routing:

    // /dispatch/* -> new Blazor app

    // everything else -> existing WebForms app

    The Business Logic Usually Survives Intact

    The part people worry about most, the actual business rules buried in code behind files, is usually the part that survives a migration best. Extracting that logic into plain C# classes with no dependency on the WebForms page lifecycle means it can be called from both the old pages and the new Blazor components during the transition, and it becomes properly testable for the first time, often years after it was written.

    This is also where the real value of the migration shows up. Not “it looks modern now,” but “we can finally write a unit test for the freight calculation logic that nobody has dared touch since 2014.”

    What Blazor Actually Buys You

    Beyond the obvious modern component model, the concrete wins for a logistics operations team tend to be:

    1. Real time updates without full page postbacks, which matters when dispatchers need to see order status change live.
    2. A component model that lets you reuse UI logic across screens instead of copying markup between pages.
    3. A path to a genuinely responsive interface for warehouse staff using tablets, which WebForms never handled gracefully.

    Set Realistic Expectations

    A WebForms to Blazor migration done screen by screen, prioritised by actual business risk, typically takes months, not weeks, for a system of meaningful size. That is not a failure of planning. It reflects the fact that the system has been absorbing business logic for years and deserves to be moved carefully rather than quickly.

    The goal is not to declare the old system dead on a fixed date. It is to reach a point where the screens that matter most are on a framework your team can actually build on, while the rest keeps running exactly as it has, until its turn comes.

  • Before Any AI Project, We Ask One Question First

    Before Any AI Project, We Ask One Question First

    Most companies approaching an AI integration in 2026 have already decided they need one. The board asked for it, a competitor announced something, or a vendor made a compelling pitch. Very few have stopped to ask whether the specific use case actually benefits from AI, or whether a well designed rule based system would be cheaper, faster to build, and easier to maintain.

    That question comes first, before any architecture discussion. Here is how we work through it, and what genuinely justified AI integration looks like once the answer is yes.

    The Question That Gets Skipped

    Ask this before anything else: does the task require judgment on ambiguous input, or does it require consistent execution of known rules? Sorting invoices by vendor name is a rules problem. Extracting a total from an invoice that arrives in twelve different formats, some scanned, some handwritten notes in the margin, is closer to a judgment problem, and that is where AI genuinely earns its complexity.

    A surprising number of “AI projects” we get asked to scope turn out to be the first kind. When that happens, the honest recommendation is to build the simpler system. It costs less, it is easier for your own team to maintain afterward, and it will not silently degrade in ways that are hard to detect.

    Where AI Actually Belongs in a .NET System

    When the answer genuinely points to AI, the integration work tends to fall into a few recurring patterns:

    Document intelligence. Companies receiving invoices, forms, or contracts that are currently keyed in by hand. Azure Document Intelligence or a similar service extracts structured data, which then flows into the existing .NET backend and SQL Server without disrupting the workflow already in place.

    Semantic search over existing data. Legacy applications where keyword search returns results that technically match but are not actually useful. Adding embeddings on top of existing SQL or Cosmos DB data lets users search in natural language and find what they meant, not just what they typed.

    var embedding = await openAiClient.GetEmbeddingsAsync( deploymentName: "text-embedding-3-large", input: userQuery);

    Narrative summaries on existing dashboards. A Power BI dashboard that shows the numbers but requires someone to interpret them can gain a natural language summary layer that explains what changed and why it matters, generated on top of data you already have.

    What Genuine Integration Requires, Beyond the Model

    The hard part of an AI project is rarely the model call itself. Most teams can wire up an API in an afternoon. The real work is everything around it: handling the cases where the model is uncertain, deciding what happens when confidence is low, logging enough to debug a wrong answer six weeks later, and making sure latency does not break a workflow that used to be instant.

    This is also the most common reason AI pilots work well in a demo and then quietly fail once they reach production. The demo tested the happy path. Production tests every edge case the demo never saw, at volume, with real user patience wearing thin.

    The Honest Version of AI Positioning

    We are not an AI company building models from scratch. We are a .NET team that reads your existing system first, asks whether AI genuinely belongs in a specific part of it, and if it does, builds the integration properly, including the parts that never make it into a product demo. If the honest answer is that you do not need AI for a given problem, we will say so, and suggest what we would build instead.

    That is a less exciting pitch than “add AI to everything.” It also tends to be the one that still works eighteen months later.

  • .NET 8 vs .NET 10: What Actually Changed for Production Teams

    .NET 8 vs .NET 10: What Actually Changed for Production Teams

    Every .NET release comes with a changelog long enough to skim past. Most of it does not matter to a team shipping a real product. This is a shorter list: what changed between .NET 8 and .NET 10 that is actually worth planning around if you are running a production SaaS application.

    Native AOT Got Genuinely Usable

    Native AOT existed in .NET 8, but the supported surface area was narrow enough that most teams left it alone. In .NET 10, more of the standard library and common libraries work correctly under AOT, which means faster startup and a smaller memory footprint are now realistic for more than toy console apps. For a SaaS product running many small services, that translates directly into lower compute cost per instance and faster cold starts, which matters a great deal if you scale horizontally or run serverless workloads.

    The honest caveat: if your codebase leans heavily on reflection or dynamic code generation, AOT still needs real testing before you commit. It is worth a spike, not a blind upgrade.

    // Publish with AOT

    dotnet publish -c Release -r linux x64 -p:PublishAot=true

    The Garbage Collector Tuning Options Improved

    .NET 10 exposes more granular server GC tuning than .NET 8 did, particularly useful for high throughput API workloads with bursty allocation patterns. If your team has ever fought GC pauses during traffic spikes, this alone can justify the upgrade. It will not fix a memory leak, but it gives you more honest levers to pull once the leak is fixed.

    Minimal APIs Matured

    Minimal APIs were usable in .NET 8 but felt incomplete for anything beyond small services: validation, OpenAPI generation, and route grouping all needed workarounds. By .NET 10, most of that friction is gone. For a new SaaS build, this is the strongest argument for greenfield teams to skip controller based APIs entirely and start with minimal APIs from day one, unless the team already has strong conventions built around controllers.

    What Did Not Meaningfully Change

    Entity Framework Core’s core query behaviour is stable across both versions. If your pain points are around EF performance, the fix is almost always in how queries are written, not the runtime version. Do not expect an upgrade to quietly solve an N+1 query problem.

    Dependency injection, configuration, and hosting all remain conceptually the same. Teams sometimes assume a major version bump means a rewrite of startup code. It does not.

    Should You Upgrade an Existing SaaS Product

    For most teams already on .NET 8, the honest answer is: upgrade, but do not rush it. .NET 8 is a long term support release with real staying power, so there is no urgency driven by end of support. The case for moving to .NET 10 is strongest when:

    1. You are compute cost sensitive and Native AOT could meaningfully cut your hosting bill.
    2. You are actively fighting GC related latency spikes under load.
    3. You are starting a new service and would rather build on the newer API surface than migrate it later.

    If none of those apply, .NET 8 will serve you well for a while yet. Version upgrades are not free. They cost testing time, and that time is better spent when there is a concrete benefit waiting on the other side.

    For a new greenfield build starting today, .NET 10 with minimal APIs and Native AOT evaluated early is a reasonable default. For an existing production system, plan the move deliberately around one of the three reasons above rather than upgrading simply because a newer number exists.

  • Signs Your .NET Framework App Is a Business Risk, Not Just Old

    Signs Your .NET Framework App Is a Business Risk, Not Just Old

    “It still works” is not the same thing as “it’s fine.” Most companies don’t decide to modernise a legacy .NET Framework application because someone made a strategic call. They decide because something broke on a Friday afternoon, the person who understood that part of the system left two years ago, and nobody can say with confidence what else is quietly at risk.

    Here is how to tell the difference between an old system that’s simply old, and one that has become an active liability, before it forces the decision for you.

    1. Nobody Can Explain Why a Piece of Code Exists

    Every long lived codebase accumulates logic nobody fully understands anymore: a strange conditional, a hardcoded value, a workaround for a bug in a library version from a decade ago. That’s normal. The risk isn’t the mystery code itself. It’s when nobody on the current team can even investigate it safely.

    If a change request gets quietly deprioritised because “we’re not sure what that part does,” that’s not a scheduling problem. It’s a sign the system has outgrown the team’s shared understanding of it, and every future change carries hidden risk.

    2. Your Hosting or Runtime Is Approaching End of Support

    .NET Framework itself isn’t going away overnight, but the surrounding stack often is: older Windows Server versions, IIS configurations, and dependent libraries all have support timelines. When any of these reach end of life, you lose access to security patches at exactly the moment the application is hardest to touch.

    A useful exercise: list every piece of infrastructure your application depends on (OS version, .NET Framework version, SQL Server version, any third party components) and check each one’s support end date. If more than one is within 18 months, that’s a business risk, not an IT footnote.

    3. Onboarding a New Developer Takes Months, Not Weeks

    This is one of the clearest tells. In a healthy codebase, a competent developer becomes productive within a few weeks. In a system that has become a liability, onboarding stretches to months, not because the developer is slow, but because the knowledge needed to work safely in that codebase was never written down.

    // A small but real example of the kind of thing that // only makes sense with tribal knowledge: if (order.Status == 4 && !order.Flags.HasFlag(OrderFlags.Legacy)) { // DO NOT REMOVE. Breaks EOD batch job for the Manchester warehouse order.Status = 7; }

    Comments like that are a symptom, not a fix. They are a sign the business logic and the documentation parted ways a long time ago.

    4. Every Deployment Needs a Tribal Knowledge Person on Standby

    If releases only go smoothly when one specific person is available, not because of process, but because they’re the only one who remembers the manual steps, the gotchas, and what to check afterward, that’s a single point of failure with a name and a phone number. Ask what would happen to the next three releases if that person took a month off. If the honest answer is “we would slow down significantly,” the risk is already priced into your roadmap whether it’s visible on it or not.

    5. The System Carries More Weight Than It Was Designed For

    Systems built for a specific scope tend to keep absorbing responsibility over the years: more integrations bolted on, more edge cases handled, more departments depending on it, without ever being redesigned for that expanded role. A tool that started as an internal reporting utility can end up, ten years later, quietly running order fulfilment for hundreds of locations. The risk shows up when the system’s actual importance to the business has outpaced the investment made in keeping it maintainable.

    What to Do About It

    None of this means rewrite everything. In most cases that’s the wrong answer. A full rewrite of a system that has been quietly running the business for over a decade is its own significant risk, and it throws away years of accumulated correctness that a fresh build won’t have. The more realistic path is usually:

    1. Document the parts nobody understands, before the person who does understand them leaves. This alone reduces more risk than any code change.
    2. Target the highest risk dependencies first, the OS, runtime, or library versions closest to end of support, rather than modernising evenly across the whole system.
    3. Get the system onto a supported .NET version incrementally, module by module, rather than as a single high stakes cutover.
    4. Bring in someone who can read the codebase quickly and work inside it safely without needing six months of ramp up first. This is usually the actual bottleneck, not the code itself.

    Legacy isn’t the problem. An undocumented, unmonitored, single point of failure legacy system is. The difference between the two is usually a few weeks of focused work. The trouble is finding time for those few weeks before something forces the issue.