C# Frameworks: .NET, ASP.NET Core, and EF Core

Published Updated

.NET provides the runtime and developer tools for C# applications, while ASP.NET Core handles web requests and Entity Framework Core handles relational data when an object-relational mapper fits the project.

Think of the stack as a workshop with a shared workbench. .NET is the workbench, ASP.NET Core organizes HTTP work, EF Core handles a specific class of database tasks, and NuGet supplies extra tools only when the standard set lacks a required capability.

The strongest default for a new backend is a small ASP.NET Core application with an explicit request pipeline, few dependencies, integration tests, and a deployment plan tied to a supported .NET release.

How the C# Framework Stack Fits Together

Each layer solves a different problem, so framework selection starts by naming the job the application must perform.

LayerPrimary jobUse it for
.NETRuntime and SDKAll C# application types
ASP.NET CoreHTTP application frameworkWeb apps and APIs
EF CoreRelational data mappingLINQ and migrations
NuGetPackage distributionMissing application capabilities

A command-line tool may need only the .NET SDK. A small API may use ASP.NET Core with direct SQL. A database-backed product may use all four layers because the application needs HTTP endpoints, migrations, a relational model, and a few focused packages.

Use a supported target framework and keep the SDK patched in development and CI. Major .NET versions can install side by side, so the project file remains the clearest record of the runtime the application expects.

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Nullable reference types expose missing-data risks during compilation. The web SDK also supplies the ASP.NET Core shared framework, which prevents ordinary web projects from needing separate references for every built-in web capability.

Choose .NET for the Project Constraints

.NET is strongest when an application benefits from static typing, long support windows, a mature debugger, and one toolchain across local development, CI, tests, and publishing.

The strongest project candidates share several practical constraints:

  • Long-lived HTTP APIs with many typed request and response contracts.
  • Database-backed products that need migrations and transaction boundaries.
  • Internal services integrated with Microsoft Entra ID or Azure infrastructure.
  • Background workers that share C# libraries with a web application.
  • Teams maintaining existing C# systems and operational knowledge.

A short script, browser-only interface, or tiny edge function may carry more runtime and build structure than the work needs. Python, JavaScript, or a platform-native function can provide a shorter path for those projects.

Choose from the expected maintenance work as well as the first demo. A project that will gain authentication, database migrations, scheduled jobs, and several API integrations can use the structure early. A disposable conversion script benefits more from a small executable surface.

Choose the ASP.NET Core Project Style

ASP.NET Core supports several application styles on the same runtime. Choose from the size of the HTTP surface and the kind of user interface the project needs.

  • Minimal APIs fit small APIs and internal services whose handlers stay short.
  • Controllers fit larger APIs that use filters, conventions, and shared request policies.
  • Razor Pages fit server-rendered forms and page-oriented back-office tools.
  • Blazor fits interactive C# user interfaces after the hosting model has been evaluated.

Minimal APIs offer the cleanest starting point for learning the request path. Move application rules into services as soon as an endpoint begins coordinating validation, persistence, authorization, and external calls.

Build the Request Pipeline First

Modern ASP.NET Core applications register services, build the host, add middleware, and map endpoints in Program.cs. Middleware order controls behavior because every request travels through that ordered pipeline.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IProductCatalog, ProductCatalog>();

var app = builder.Build();

app.UseHttpsRedirection();

app.MapGet("/products", async (
    IProductCatalog catalog,
    CancellationToken cancellationToken) =>
{
    var products = await catalog.FindActiveAsync(cancellationToken);
    return Results.Ok(products);
});

app.Run();

public partial class Program { }

The service container owns IProductCatalog, while the endpoint owns the HTTP response. Cancellation flows into the service so a disconnected request or application shutdown can stop database and network work that no longer has a caller.

Keep the pipeline readable from top to bottom. Authentication must run before authorization, exception handling must wrap the code it protects, and endpoint mapping must happen before the application starts.

Add EF Core When the Data Model Needs It

EF Core maps C# types to relational tables, translates LINQ into provider-specific SQL, tracks changes, and creates schema migrations. It fits product applications that benefit from a persistent domain model and reviewed schema history.

On the workshop bench, EF Core is the tool for mapping application objects to relational work. It does not replace schema review, query plans, or database constraints.

namespace Store.Api;

public sealed class StoreDbContext : DbContext
{
    public StoreDbContext(DbContextOptions<StoreDbContext> options)
        : base(options)
    {
    }

    public DbSet<Product> Products => Set<Product>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>(entity =>
        {
            entity.HasKey(product => product.Id);
            entity.Property(product => product.Name)
                .HasMaxLength(200)
                .IsRequired();
        });
    }
}

The namespace uses ordinary C# dot notation. C# namespaces never contain path separators or backslashes.

Keep concrete database knowledge alongside the application model. Column types, unique constraints, indexes, transaction boundaries, and provider behavior still decide whether the application preserves data correctly. Inspect generated SQL when a query becomes slow, and review every migration before production.

dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet ef migrations add AddProducts
dotnet ef database update

Applications dominated by reporting SQL, bulk operations, or provider-specific queries may use direct SQL or a smaller data-access layer. The selection criterion is control over the real database workload.

Keep NuGet Dependencies Deliberate

NuGet extends the application when the shared framework lacks a required capability. A database provider, an OpenTelemetry exporter, or a test host has a clear job; a copied package list usually carries stale assumptions.

Treat NuGet as the workshop tool cabinet: take a package only when the shared workbench lacks the required capability, then return to the dependency list when that requirement disappears.

dotnet list package
dotnet list package --outdated
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet remove package Unused.Dependency

Review every package addition for maintenance, target-framework compatibility, transitive dependencies, licensing, and security advisories. Keep test-only packages inside test projects, and remove packages whose capability has moved into the shared framework.

The ASP.NET Core NuGet package guide applies those criteria to common package families.

Separate Authentication from Authorization

Authentication identifies the caller, while authorization decides which resources and operations that identity can access. Cookies, bearer tokens, OpenID Connect, API keys, and machine credentials have different threat models.

An API accepting OAuth or OpenID Connect access tokens can use Microsoft.AspNetCore.Authentication.JwtBearer. Configuration must validate the token issuer, audience, signature, and expiration before the application trusts its claims.

Authorization policies should express application rules such as tenant membership or permission to edit a product. A check for an authenticated identity only proves that a caller signed in; it leaves the resource decision unresolved.

Keep production secrets outside source files and deployment artifacts. ASP.NET Core configuration can combine local JSON defaults with environment variables and a platform secret store, while environment checks keep developer exception pages and permissive test settings away from production.

Test the Application that Will Run

Unit tests fit isolated business rules with few external dependencies. Integration tests fit routing, middleware, service registration, authentication policies, JSON contracts, and database-provider behavior because those risks appear only when the application boots.

The integration-test project is the test bench for the assembled application, where framework wiring and application code have to work together.

public sealed class ProductApiTests
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public ProductApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task ProductsEndpointReturnsSuccess()
    {
        var response = await _client.GetAsync("/products");

        response.EnsureSuccessStatusCode();
    }
}

WebApplicationFactory exercises the real host with test configuration. The public partial class Program declaration after app.Run() exposes the top-level entry point to the test project. Database tests should use the production provider or a close substitute when constraints, transactions, migrations, generated columns, or provider functions affect correctness.

Design Background and Outbound Work

Hosted services support short background tasks inside an ASP.NET Core process. Dedicated worker projects suit queue consumers and jobs that need separate scaling, longer retry policies, or independent deployment.

A background service must pass its shutdown token into delays, database calls, and external requests. It should create a dependency-injection scope before resolving scoped services such as a DbContext.

Outbound HTTP calls need similarly explicit lifetime and failure ownership. Use long-lived clients or IHttpClientFactory, configure timeouts, and retry only operations that are safe to repeat. Payment commands and order submissions need idempotency before an automatic retry can be considered safe.

Make Production Behavior Visible

ASP.NET Core provides structured logging through ILogger, and .NET exposes first-party hooks for metrics and traces. OpenTelemetry can export those signals to a collector or monitoring backend.

_logger.LogInformation(
    "Completed product import for batch {BatchId}",
    batchId);

Structured placeholders keep BatchId queryable as data. Exclude tokens, cookies, connection strings, secrets, and unfiltered request bodies from logs.

A production service needs enough evidence to answer which endpoint failed, which dependency slowed down, and which deployment changed the behavior. Instrument the HTTP pipeline, database boundary, and outbound calls that decide the user-visible result.

Publish with a Runtime Plan

The dotnet publish command creates the application output that a host or container will run. Build the release configuration in CI so the deployable artifact comes from the same tested source state.

dotnet test --configuration Release
dotnet publish Store.Api --configuration Release --output ./publish

A framework-dependent deployment expects a compatible runtime on the host. A self-contained deployment includes the selected runtime, which increases the artifact and moves runtime patching into the application's release process. Containers also need base-image updates because an old runtime remains old inside a reproducible image.

Record four operational decisions before production: who patches the runtime, how migrations are applied, where logs and traces are collected, and how the previous release is restored. Health checks should cover the service process without turning every dependency outage into an automatic restart loop.

Use a Project Layout with Clear Owners

A single project is enough while the application stays small. Split projects when separate responsibilities begin changing for separate reasons.

src/
  Store.Api/
  Store.Application/
  Store.Infrastructure/
tests/
  Store.Api.Tests/
  Store.Application.Tests/

The API project owns HTTP contracts and framework wiring. The application project owns use cases and business rules. Infrastructure owns EF Core, queues, file storage, and external service clients. This layout is useful only when those boundaries reduce coupling in real changes.

Common Pitfalls & Debugging

Middleware Runs in the Wrong Order

Symptom: authorization fails unexpectedly or errors bypass the configured handler. Cause: middleware was registered after the component it must protect. Fix: read Program.cs in request order, place exception handling early, and keep authentication before authorization.

EF Core Generates an Expensive Query

Symptom: a page triggers many database calls or scans more rows than expected. Cause: hidden lazy loading, an unbounded query, or a missing index changed the SQL workload. Fix: inspect generated SQL, project only required columns, bound the result, and check the database plan.

A Background Service Fails at Startup

Symptom: the host reports a lifetime error for DbContext or another scoped service. Cause: a singleton hosted service requested a scoped dependency from its constructor. Fix: inject IServiceScopeFactory, create a scope for each unit of work, and dispose it promptly.

Frequently Asked Questions

Is .NET only for Windows applications?

.NET runs on Windows, Linux, and macOS, and ASP.NET Core services commonly run in Linux containers. Windows remains a strong fit for applications tied to Microsoft infrastructure, while the runtime itself supports cross-platform deployment.

Should a new API use Minimal APIs or controllers?

Minimal APIs suit small endpoint sets with direct service boundaries. Controllers suit larger APIs that benefit from filters, conventions, model binding policies, or an established MVC structure. Start with the smallest style that keeps handlers readable.

Does every ASP.NET Core app need EF Core?

No. EF Core suits applications that benefit from LINQ queries, migrations, change tracking, and provider integration. Direct SQL or another data-access library can fit reporting services, small query surfaces, or workloads that need precise SQL control.

When should a web app use a separate worker?

Use a separate worker when background work needs independent scaling, queue consumption, long retries, or deployment controls. A short cleanup task can stay in the web process when shutdown, failure, and resource use remain predictable.

When should middleware be added to the request pipeline?

Add middleware when matching requests need the same cross-cutting behavior, such as exception handling, HTTPS redirection, authentication, or authorization. Place it before the endpoints or middleware it must wrap, and keep endpoint-specific rules in the endpoint when that owner is clearer.

How should EF Core migrations reach production?

Generate migrations from reviewed model changes, inspect the SQL they produce, and apply them through one controlled deployment step. Avoid letting every application instance race to migrate the same production database during startup.

Self-Check

  1. Multiple choice: Which layer handles the ASP.NET Core HTTP request pipeline: .NET, ASP.NET Core, or EF Core?
  2. Multiple choice: Which project style best fits a page-oriented server-rendered admin tool: Minimal APIs, Razor Pages, or a worker service?
  3. Multiple choice: Where should Microsoft.AspNetCore.Mvc.Testing be referenced: the production API project or the integration-test project?
  4. Predict the result: What response body does app.MapGet("/status", () => Results.Ok("ready")); return for a successful request?
  5. Predict the result: What happens when a singleton hosted service requests a scoped DbContext directly from its constructor while scope validation is enabled?

Answers

  1. ASP.NET Core. .NET supplies the runtime, while ASP.NET Core owns web middleware and endpoint routing.
  2. Razor Pages. Its page-oriented model fits server-rendered forms and back-office screens.
  3. The integration-test project. The package boots the application for tests and does not belong in the production dependency set.
  4. ready. The endpoint returns an HTTP 200 response whose JSON string value is "ready".
  5. The host fails validation or startup. Create a scope inside the worker before resolving the scoped database context.

Next Steps

Start with a small API, one real database operation, and an integration test that boots the host. Add framework surface only when the application has a named requirement for it.

Continue with the ASP.NET Core NuGet package guide, then use the SQL programming guide to review schema design, joins, indexes, and transactions beneath the data-access layer.

Sources

  1. [1]
    Releases and support for .NET
    (learn.microsoft.com)
  2. [2]
    C# documentation
    (learn.microsoft.com)
  3. [3]
  4. [4]
    Entity Framework Core
    (learn.microsoft.com)
  5. [5]
  6. [6]
  7. [7]
    dotnet CLI NuGet commands
    (learn.microsoft.com)
  8. [8]
  9. [9]
    HttpClient guidelines for .NET
    (learn.microsoft.com)
  10. [10]
    Logging in C#
    (learn.microsoft.com)
  11. [11]
  12. [12]