.NET

Software Mile builds on .NET – Microsoft’s modern, cross-platform framework and the natural choice for organizations invested in the Microsoft stack or building high-performance services in C#.

Modern .NET, Cross-Platform

  • ASP.NET Core web applications and APIs, running on Windows or Linux
  • C# services with the performance and tooling the modern .NET platform brings
  • Integration with Azure, SQL Server, and the Microsoft ecosystem
  • Migration of legacy .NET Framework applications to modern .NET

.NET is a strong, fast, well-tooled platform – especially in a Microsoft shop. Tell us what you are building.

A .NET Framework Migration Is Decided by the Dependencies

Class libraries, data access and business logic usually port to modern .NET with limited change. What decides the size of the job is everything else the application touches, which is why the dependency inventory comes before anyone quotes. Presentation layers and platform-specific integrations are where a migration turns into a rebuild, and an honest assessment names those first.

Sometimes the inventory concludes that migration is not worth doing yet. An internal application with a small user base and no pending change requests can be left where it is, on a supported operating system and in a controlled network position, until there is a business reason to move it. Four things usually decide which case you are in:

  • Web Forms, which has no direct equivalent, so the presentation layer is rewritten rather than ported.
  • Server-side WCF, which needs a move to REST or gRPC, or a port to CoreWCF where the contract has to stay as it is.
  • Windows-specific APIs and COM interop, which constrain where the result can run.
  • Third-party components with no modern .NET release, which need replacing.

Does Modern .NET Mean You Have to Leave Windows?

No. Cross-platform is a capability, not an obligation, and Windows hosting stays reasonable when you have directory integration, established IIS operations, or an operations team built around it. The gain from Linux containers is mostly hosting density, licensing and fitting into an orchestration platform you already run.

This decision belongs to whoever operates the system as much as to whoever writes it. If your operations team runs Windows and has no Linux experience, moving the runtime hands them a platform they cannot troubleshoot under pressure, and the hosting economics have to be worth quite a lot before that trade makes sense.

Blazor, Razor Pages, or a JavaScript Front End?

Blazor lets a C# team build interactive interfaces without a separate JavaScript stack, which is valuable when that team is who you have. The server-hosted model keeps the download small but needs a persistent connection and is sensitive to latency. The WebAssembly model tolerates intermittent connectivity at the cost of a larger initial download.

Razor Pages and MVC remain the simplest answer for applications that are mainly forms and records, and being unfashionable is not an argument against them. A React or Angular front end against an ASP.NET Core API keeps the boundary explicit and the two hiring pools separate, at the cost of running two stacks.

Are You Choosing .NET, or Are You Choosing Azure?

These are separate decisions that often get made as one. Modern .NET runs on Linux, in containers, on the major cloud platforms and on hardware you own. Azure is a convenience if you are already there, since identity, hosting, managed databases, secrets and telemetry are integrated and reduce how much you have to assemble.

If your infrastructure sits elsewhere, that is neither a reason to avoid .NET nor a reason to move. Treat a platform migration as its own project with its own justification. Folding it into a runtime upgrade is how two manageable pieces of work become one that nobody can estimate.

Where .NET Projects Accumulate Work After Launch

The parts of a .NET project that get attention during a build are the features. The parts that cause trouble two years later are the mechanics around them: schema changes moved between environments by hand, configuration that varies per environment in ways nobody wrote down, and secrets living wherever they were first convenient.

None of that is hard to get right at the start, and all of it is tedious to unpick once the application is in production and a second team is contributing. Four decisions are worth making before the first release:

  • A target framework version for new work, so the estate does not drift into four runtimes at once.
  • Schema changes applied by a migration that runs as part of the deploy.
  • Configuration declared per environment, with the differences visible in one place.
  • Secrets held in a store the application reads at startup, and nowhere else.

How Long a .NET Version Is Supported Decides When You Upgrade

Modern .NET ships a major release every November. Even-numbered releases are Long Term Support and are serviced for three years; odd-numbered releases are Standard Term Support and are serviced for eighteen months. Support ends on a published date whether or not anything is wrong with the application, and once it ends the runtime stops receiving security fixes, so the upgrade gets scheduled by the calendar rather than by the feature list.

The version an application targets lives in the target framework moniker in the project file, and runtimes install side by side, so one host can carry several and each application binds to the one it was built against. A library that has to be referenced from both .NET Framework and modern .NET targets netstandard2.0, which is the last version .NET Framework can consume, or multi-targets and carries conditional code for each.

Upgrading a single project is usually a smaller job than the things around it that hold the version in place:

  • Package dependencies, whose own minimum target eventually moves past yours, at which point fixes stop arriving on the version you are sitting on.
  • The operating system or container base image, which carries its own end of support date that rarely lines up with the runtime's.
  • global.json, which pins the SDK a build uses and stops a newer developer machine from quietly building with a different toolchain.
  • A managed hosting platform, which offers a fixed list of runtime versions and retires them on its own schedule rather than yours.

Three Causes That Look Like the Same Slowdown

An ASP.NET Core service that is comfortable at low load and unresponsive at high load usually has one of three causes underneath, and from the outside they present identically: latency climbs, then requests time out. Requests-per-second and queue depth alone will not separate them. They are distinguishable with dotnet-counters against the System.Runtime and Microsoft.AspNetCore.Hosting providers, which is worth doing before anyone adds instances, since more instances means more connections against the same pooled resource, so scaling out makes pool exhaustion worse rather than better.

  • Thread pool starvation, which shows as queue length climbing while the thread count grows one thread at a time, because that is the rate the pool injects threads past its minimum. The cause is blocking on a request thread, usually .Result, .Wait() or GetAwaiter().GetResult() somewhere on the path, and the fix is an async call chain end to end rather than a higher minimum thread count.
  • Connection pool exhaustion, which shows as requests waiting and then failing at the pool's timeout rather than at the query's. The default maximum pool size in ADO.NET is 100 per distinct connection string, and the usual reason for reaching it is a connection held open for the length of a request instead of the length of a query.
  • GC pressure, which shows as percent time in GC and Gen 2 collection counts rising in step with load. Large short-lived buffers are the common source, since arrays over 85,000 bytes go straight to the large object heap, and in a container the effect is sharper because the runtime sizes its heap from the cgroup memory limit rather than from the host's total memory.

Configuration Resolves Differently on the Server Than on a Laptop

Applications that behave locally and misbehave once deployed usually differ in how configuration resolves rather than in code. One of the first things to check is which environment name the host is actually reporting: ASPNETCORE_ENVIRONMENT selects which appsettings.{Environment}.json is layered on top of appsettings.json, and when the variable is unset the host defaults to Production, so a setting that only ever existed in the Development file is simply absent, with no error to read.

Providers are applied in order and later ones win: appsettings.json, then the environment-specific file, then user secrets in Development, then environment variables, then command line arguments. Environment variables express nesting with a double underscore rather than a colon, so Logging:LogLevel:Default is set as Logging__LogLevel__Default, and a variable written with a colon on a Linux host neither binds nor complains.

Three more differences only surface once something sits in front of the application:

  • Behind a reverse proxy or an ingress controller, Request.Scheme stays http until forwarded headers middleware is configured to trust X-Forwarded-Proto, and until then generated absolute URLs and OAuth redirect URIs come back as http and fail validation at the identity provider.
  • Data protection keys are written inside the container by default, so authentication cookies and antiforgery tokens issued before a restart, or by a different replica, are rejected. The key ring has to be persisted to shared storage and the application name set explicitly so replicas agree on it.
  • Globalization differs by image. Alpine-based images carry no ICU unless icu-libs is installed, and invariant globalization mode changes culture-sensitive parsing, sorting and string comparison, which surfaces as dates and decimals read differently than they were on Windows.

Running ASP.NET Core Under IIS

IIS does not host a modern .NET application the way it hosted .NET Framework. The server needs the ASP.NET Core Hosting Bundle, which installs the ASP.NET Core Module along with the runtime, and the application pool is set to No Managed Code, because the pool is not loading .NET Framework. Publishing emits a web.config that points IIS at the module, and because that file is generated, hand edits to it on the server are lost on the next publish unless they are made to the source web.config in the project.

In-process hosting runs the application inside the IIS worker process and is the default. Out-of-process runs Kestrel as a separate process that IIS proxies to over a loopback port. Under IIS the request body size is capped in two places, Kestrel's own maximum request body size and maxAllowedContentLength in web.config, so an upload can be rejected by whichever limit was not raised, and the error returned differs depending on which one fired.

How a Schema Change Actually Reaches Production

Calling Database.Migrate() from application startup is the version that is easiest to write and the first to break. With more than one replica starting at the same moment, several processes attempt the same migration at once, and the application's own database account has to hold schema-altering rights for the entire time it is running rather than only during a release.

Two alternatives keep the migration inside the release rather than inside the application. A migration bundle, produced by dotnet ef migrations bundle, is a self-contained executable that the deploy step runs with the connection string supplied at run time, so the runtime account never needs DDL rights. Generating a script with the idempotent option instead produces SQL that can be read and reviewed by whoever owns the database and is then applied by that same release step, which suits a database owned by a different team with its own change process without the change being moved by hand.

Whichever route runs them, migrations have behavior worth knowing before the first branch merge:

  • Migrations are ordered by the timestamp in their file name, so two branches that each add one produce a conflict in the model snapshot that a text merge will happily resolve into something that applies cleanly and describes the wrong model. Regenerating the later migration after the merge is the reliable fix.
  • A column rename is scaffolded as a drop and an add unless the migration is written as RenameColumn, so the generated version compiles, applies, and discards the data in that column.
  • Down migrations are not a rollback path once the new version has written data. A forward fix is the realistic plan, and the down method is mostly useful during development.
  • Data backfills are not generated. EF Core scaffolds schema operations only, so moving or populating data belongs in its own migration or its own reviewed script.

Frequently Asked Questions

Is modern .NET the same thing as .NET Framework?

No. .NET Framework is the Windows-only runtime that ships with the operating system and is patched with it; 4.8.1 is its final version, and it receives fixes rather than new features. Modern .NET is a separate cross-platform runtime that installs alongside the application and is versioned independently of Windows. Moving code between them is a port rather than a configuration change.

What stops working if we move an existing application to Linux?

Desktop UI frameworks do not run there at all, so anything with a WinForms or WPF surface stays on Windows. Registry access, hosting as a Windows service, and P/Invoke into Win32 libraries all need replacing, and System.Drawing.Common is Windows-only from .NET 6 onward, so image work moves to ImageSharp or SkiaSharp. The quieter one is the file system: paths are case sensitive and the separator is a forward slash, so hardcoded backslashes and casing that was never consistent fail at run time rather than at build time.

What has to be installed on a server to run a .NET application?

A framework-dependent publish needs the matching ASP.NET Core runtime already present on the host. By default it rolls forward to the latest patch of the same major and minor version, but never across a major version, so an application built for one major release will not start on a host that only carries the next. A self-contained publish carries the runtime inside its own output and needs nothing installed, at the cost of a larger deployment and of patching that runtime yourself with every release.

Can we keep hosting on IIS?

Yes, and Windows Authentication continuing to work is often the reason IIS stays in the picture. The choice worth making deliberately is the hosting model: out-of-process is the fallback for when something about the application does not behave inside the worker process, such as a native dependency that conflicts with what else the pool has loaded. It is set by the AspNetCoreHostingModel property in the project file rather than in code, so it can be changed and reverted with a republish.

What has to exist before an ASP.NET Core application can be deployed?

There has to be a DNS name and a certificate, with a decision about where TLS terminates, since terminating at a proxy changes what the application sees on every request. There has to be a database account scoped to what the application does at run time, kept separate from whatever account applies schema changes. And there has to be an outbound network path from the host to everything the application calls, including the identity provider, the mail relay and any partner API, which is the item most often missed wherever egress is filtered.

What do you need from us to scope a .NET migration?

Start with the solution and project files, or read access to the repository, so the target frameworks and the project graph can be read rather than described. Then the package list with exact versions and the current license status of each, including anything pulled from a private feed and anything referenced as a checked-in DLL with no package behind it. Finally the deployment topology and the list of systems that call in and that are called out to, since those decide what can change without coordinating with another team.

What is the practical difference between Blazor Server and Blazor WebAssembly once it is deployed?

The differences that matter after launch are infrastructure ones. Blazor Server holds a WebSocket circuit per user, so every proxy, load balancer and ingress in front of it has to allow WebSocket upgrades, and running more than one instance needs session affinity or a reconnect lands on a node that does not hold the circuit. Blazor WebAssembly ships its assemblies to the browser, where they can be downloaded and read, so nothing embedded in the client is private and every rule that matters has to be enforced again in the API. Both models call the same server-side API surface, so revisiting the choice later costs the component layer rather than the back end.