⏳
Loading cheatsheet...
Modern .NET platform architecture, C# evolution, runtime internals, ASP.NET Core, EF Core, testing, and deployment.
| Version | Type | Release | EOL | Runtime |
|---|---|---|---|---|
| .NET 8 | LTS | Nov 2023 | Nov 2026 | CoreCLR 8.0 |
| .NET 9 | STS | Nov 2024 | May 2026 | CoreCLR 9.0 |
| .NET 10 | LTS (Preview) | Nov 2025 | Nov 2030 | CoreCLR 10.0 |
| .NET 6 | LTS | Nov 2021 | Nov 2024 | CoreCLR 6.0 |
| Platform | Status | Use Case | OS |
|---|---|---|---|
| .NET (8/9/10) | Active | Modern apps, cross-platform | Win, Mac, Linux |
| .NET Framework | Legacy | Windows-only apps | Windows only |
| Mono | Integrated | Mobile, WebAssembly via .NET | Cross-platform |
| .NET MAUI | Active | Native mobile + desktop | iOS, Android, Win, Mac |
| Component | Description |
|---|---|
| CLR | Common Language Runtime — JIT compiler, GC, thread management |
| CoreCLR | Cross-platform CLR (open source, .NET 5+) |
| JIT (RyuJIT) | Just-In-Time compilation — compiles IL to native at runtime |
| AOT | Ahead-Of-Time — compile to native at build time (Native AOT or ReadyToRun) |
| Native AOT | Full AOT — no JIT at runtime, smallest binary, fastest startup |
| R2R (ReadyToRun) | Pre-JIT — reduced startup time, still JIT-capable |
| TFM | Target | Notes |
|---|---|---|
| net9.0 | .NET 9 | Latest STS (Standard Term Support) |
| net8.0 | .NET 8 | Current LTS — recommended for production |
| net10.0 | .NET 10 | Next LTS — in preview |
| netstandard2.0 | .NET Standard 2.0 | Max library compatibility (Framework + Core) |
| netstandard2.1 | .NET Standard 2.1 | Core 3.0+ only |
| net48 | .NET Framework 4.8 | Legacy Windows apps |
netstandard2.0 for shared libraries that must work across .NET Framework and .NET.| Version | Key Features |
|---|---|
| C# 8 | Nullable refs, async streams, ranges/indexes, using declarations, default interface methods |
| C# 9 | Records, init-only setters, top-level statements, pattern matching enhancements |
| C# 10 | Global usings, file-scoped namespaces, record structs, const interpolated strings |
| C# 11 | Raw string literals, generic math, required members, list patterns, UTF-8 strings |
| C# 12 | Primary constructors for classes, collection expressions, inline arrays, ref readonly params |
| C# 13 | params collections, new lock object, partial properties, implicit indexers, field keyword |
| Feature | Description |
|---|---|
| params collections | params now accepts any collection with Add(), not just arrays |
| new lock object | System.Threading.Lock type — thread-safe synchronization primitive |
| partial properties | Allow partial properties in partial classes (like partial methods) |
| field keyword | Access the backing field in property accessors directly |
| ref struct improvements | ref struct fields in classes (scoped ref) |
| implicit indexers | "obj[key]" is now implicitly an indexer (C# 13) |
// ── Primary Constructors for Classes (C# 12+) ──
public class ProductService(string connectionString, ILogger logger)
{
// Parameters become fields automatically
public List<Product> GetAll() => _db.Query(connectionString);
}
// ── params Collections (C# 13) ──
public void PrintItems(params ReadOnlySpan<string> items)
=> Console.WriteLine(string.Join(", ", items));
PrintItems("a", "b", "c"); // Works with any Add()-capable type
// ── field keyword (C# 13) ──
public class Person
{
private string _name = "Unknown";
public string Name
{
get => field;
set => field = value.Trim();
}
}
// ── System.Threading.Lock (C# 13) ──
public class SafeCache
{
private Lock _lock = new();
public void Add(string key, string value)
{
using (_lock.EnterScope())
{
// Thread-safe access
}
}
}
// ── Collection Expressions (C# 12) ──
int[] nums = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
Span<int> span = [1, 2, 3];required members for immutability. Use field keyword in C# 13 to skip manual backing field declarations.| Aspect | Minimal APIs | Controllers |
|---|---|---|
| Setup | Top-level in Program.cs | Class-based, [ApiController] |
| Best for | Small services, microservices | Large apps, complex routing |
| Filters | Manual via pipeline | [Authorize], [ValidateAntiForgeryToken] |
| Swagger | WithScalar / NSwag AddOpenApi() | Swashbuckle auto-gen |
| Binding | Parameter-level | [FromBody], [FromQuery], [FromRoute] |
| Versioning | MapApiGroup() + WithOpenApi() | Route attributes / APIExplorerSettings |
| Middleware | Purpose | Order |
|---|---|---|
| UseExceptionHandler | Global error handling | First (catch all) |
| UseHsts | HTTP Strict Transport Security | Early |
| UseHttpsRedirection | Redirect HTTP → HTTPS | Early |
| UseCors | Cross-origin requests | Before auth |
| UseAuthentication | Identify user | Before authz |
| UseAuthorization | Authorize access | After auth |
| UseRateLimiter | Rate limiting | After routing |
| UseOutputCache | Response caching | Late |
| UseAntiforgery | CSRF protection | Before endpoints |
// ── Minimal API (ASP.NET Core 9) ──
var builder = WebApplication.CreateBuilder(args);
// Services
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();
// Rate Limiting
builder.Services.AddRateLimiter(opt => opt
.AddFixedWindowLimiter("fixed", w =>
{
w.Window = TimeSpan.FromMinutes(1);
w.PermitLimit = 100;
}));
var app = builder.Build();
app.UseRateLimiter();
app.MapOpenApi();
app.MapGet("/api/products", async (AppDbContext db) =>
await db.Products.ToListAsync())
.WithName("GetProducts")
.WithOpenApi();
app.MapGet("/api/products/{id:int}", async (int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product p
? Results.Ok(p) : Results.NotFound());
app.MapPost("/api/products", async (Product p, AppDbContext db) =>
{
db.Products.Add(p);
await db.SaveChangesAsync();
return Results.Created($"/api/products/{p.Id}", p);
}).RequireRateLimiting("fixed");
app.Run();// ── Model Validation ──
public class CreateProductDto
{
[Required] [MaxLength(100)]
public string Name { get; set; } = string.Empty;
[Range(0.01, 10000)]
public decimal Price { get; set; }
[EmailAddress]
public string? ContactEmail { get; set; }
}
// JSON Options (Program.cs)
builder.Services.ConfigureHttpJsonOptions(opt =>
{
opt.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
opt.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});WithOpenApi() for Swagger support. Add AddRateLimiter() for DDoS protection. For complex apps with many endpoints, Controller-based APIs offer better organization with filters, conventions, and API versioning.| Concept | Description |
|---|---|
| Controller | Handles requests, returns IActionResult (View, Json, Redirect) |
| Razor View (.cshtml) | Server-rendered HTML with @model, @foreach, Tag Helpers |
| Layout | Shared template (_Layout.cshtml) — defines common structure |
| Partial View | Reusable view snippet, rendered via <partial name="..." /> |
| View Component | Reusable component with logic (like a mini-controller) |
| Areas | Partition large apps into functional areas (Admin, Customer) |
| Tag Helpers | Server-side attributes in HTML (asp-for, asp-action, asp-route) |
| Filter | Interface | When | Use Case |
|---|---|---|---|
| Authorization | IAuthorizationFilter | First | Check access permissions |
| Resource | IResourceFilter | After auth | Caching, short-circuit |
| Action | IActionFilter | Before/after action | Logging, validation |
| Exception | IExceptionFilter | After action | Global error handling |
| Result | IResultFilter | Before/after result | Response modification |
// ── Controller ──
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetAll(
[FromQuery] int page = 1, [FromQuery] int pageSize = 20)
{
var products = await _service.GetPagedAsync(page, pageSize);
return Ok(products);
}
[HttpGet("{id:int}")]
public async Task<ActionResult<Product>> Get(int id)
{
var product = await _service.GetByIdAsync(id);
return product is null ? NotFound() : Ok(product);
}
[HttpPost]
[ProducesResponseType(typeof(Product), StatusCodes.Status201Created)]
public async Task<ActionResult<Product>> Create(
[FromBody] CreateProductDto dto)
{
var product = await _service.CreateAsync(dto);
return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
}
}
// ── Custom Action Filter ──
public class LoggingFilter : IAsyncActionFilter
{
private readonly ILogger _logger;
public LoggingFilter(ILogger<LoggingFilter> logger) => _logger = logger;
public async Task OnActionExecutionAsync(
ActionExecutingContext context, ActionExecutionDelegate next)
{
_logger.LogInformation("Executing {Action}", context.ActionDescriptor.DisplayName);
var result = await next();
_logger.LogInformation("Executed {Action} → {Status}",
context.ActionDescriptor.DisplayName,
result.HttpContext.Response.StatusCode);
}
}[ProducesResponseType] for Swagger documentation.| Model | Runtime | Pros | Cons |
|---|---|---|---|
| Blazor Server | Server (SignalR) | Full .NET, small download | Latency, server memory |
| Blazor WebAssembly | Browser (WASM) | Client-side, offline capable | Large download, limited .NET |
| Blazor United (.NET 8+) | Auto/Server/WASM | Best of both worlds | Newer, more complex |
| Blazor Web App | Full-stack template | Streaming SSR + interactivity | Configure render modes |
| Mode | Description | Use Case |
|---|---|---|
| Static SSR | Pre-rendered, no interactivity | Marketing pages, blogs |
| Interactive Server | SignalR connection, runs on server | Data-heavy, secure operations |
| Interactive WebAssembly | Runs in browser via WASM | Offline, low-latency UI |
| Interactive Auto | Starts as Server, transitions to WASM | Best experience, smart fallback |
@* ── Blazor Component (.razor) ── *@
@page "/products"
@rendermode InteractiveServer
@inject IProductService ProductService
@inject ILogger<ProductList> Logger
<h3>Product Catalog</h3>
<input @bind="searchTerm" @bind:event="oninput"
placeholder="Search products..." class="form-control" />
<Virtualize Items="@filteredProducts" Context="product" OverscanCount="10">
<div class="card p-3 mb-2">
<h5>@product.Name</h5>
<p>@product.Description</p>
<span class="badge bg-primary">$@product.Price</span>
<button @onclick="() => Delete(product.Id)" class="btn btn-danger btn-sm">
Delete
</button>
</div>
</Virtualize>
@code {
private string searchTerm = "";
private List<Product> products = new();
private List<Product> filteredProducts => products
.Where(p => p.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
.ToList();
protected override async Task OnInitializedAsync()
{
products = await ProductService.GetAllAsync();
Logger.LogInformation("Loaded {Count} products", products.Count);
}
private async Task Delete(int id)
{
await ProductService.DeleteAsync(id);
products.RemoveAll(p => p.Id == id);
}
}@rendermode InteractiveAuto for best UX. Use Virtualize for large lists. Use @attribute [Authorize] for component-level security.| Concept | Description |
|---|---|
| DbContext | Session with database — query/save via DbSet<T> |
| DbSet<T> | Represents a table, enables LINQ queries |
| POCO Models | Plain Old CLR Objects — no base class required |
| Migrations | Version-controlled schema changes (Add-Migration, Update-Database) |
| Tracking | ChangeTracker auto-detects changes (NoTracking for read-only) |
| Eager Loading | Include() — loads related data in single query |
| Explicit Loading | Entry().Collection().Load() — load on demand |
| Lazy Loading | Proxies auto-load navigations (not recommended) |
| Feature | Description |
|---|---|
| JSON Columns | Map POCOs to JSON columns (PostgreSQL, SQL Server) |
| Complex Types | Value objects mapped to table columns (no own table) |
| ExecuteUpdate/Delete | Bulk operations without loading entities |
| HierarchyId | SQL Server hierarchyid type support |
| Primitive Collections | Map List<int>, string[] to DB columns |
| Optimized Count | COUNT(*) optimization for large tables |
// ── DbContext ──
public class AppDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
public DbSet<Order> Orders => Set<Order>();
public AppDbContext(DbContextOptions<AppDbContext> opts) : base(opts) { }
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Product>(e =>
{
e.HasKey(p => p.Id);
e.Property(p => p.Name).HasMaxLength(200).IsRequired();
e.HasIndex(p => p.Sku).IsUnique();
e.HasOne(p => p.Category).WithMany(c => c.Products)
.HasForeignKey(p => p.CategoryId);
e.ComplexProperty(p => p.Dimensions, d =>
{
d.Property(x => x.Width).HasColumnName("Width");
d.Property(x => x.Height).HasColumnName("Height");
});
});
mb.Entity<Product>().OwnsMany(p => p.Tags, t =>
{
t.ToJson("TagsJson"); // EF Core 9: JSON column
});
}
}
// ── Efficient Queries ──
var products = await db.Products
.AsNoTracking() // Read-only (no tracking overhead)
.Include(p => p.Category) // Eager loading
.Include(p => p.OrderItems)
.ThenInclude(oi => oi.Order) // Multi-level eager load
.Where(p => p.IsActive)
.OrderByDescending(p => p.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(p => new ProductDto // Projection (no entity materialization)
{
Id = p.Id,
Name = p.Name,
CategoryName = p.Category.Name,
Price = p.Price
})
.ToListAsync();
// ── Bulk Update / Delete (EF Core 7+) ──
await db.Products
.Where(p => !p.IsActive && p.CreatedAt < cutoff)
.ExecuteDeleteAsync();
await db.Products
.Where(p => p.CategoryId == oldCatId)
.ExecuteUpdateAsync(s => s
.SetProperty(p => p.CategoryId, newCatId)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow));Include() for relationships or Select() projections. Use AsNoTracking() for read-only queries — up to 30% faster. Use ExecuteUpdateAsync() for bulk operations instead of loading + modifying entities.| Scheme | Use Case | Library |
|---|---|---|
| ASP.NET Core Identity | User registration/login, roles, 2FA | Microsoft.AspNetCore.Identity |
| JWT Bearer | Stateless APIs, SPAs, mobile | Microsoft.AspNetCore.Authentication.JwtBearer |
| OAuth2 / OpenID Connect | Social login (Google, GitHub), SSO | Microsoft.AspNetCore.Authentication.OpenIdConnect |
| Azure AD | Enterprise apps, Microsoft 365 | Microsoft.Identity.Web |
| API Keys | Simple API access control | Custom middleware or AspNetCore.Authentication.ApiKey |
| Strategy | How | Example |
|---|---|---|
| Role-based | [Authorize(Roles = "Admin")] | Admin-only endpoints |
| Claim-based | [Authorize(Policy = "IsManager")] | Claims from JWT/Identity |
| Policy-based | AuthorizationPolicy with requirements | Complex business rules |
| Resource-based | IAuthorizationHandler + IAuthorizationRequirement | Per-entity permissions |
| Attributes | [Authorize], [AllowAnonymous] | Controller/action level |
// ── Program.cs — JWT Authentication ──
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
builder.Services.AddAuthorizationBuilder()
.AddPolicy("RequireAdmin", p => p.RequireRole("Admin"))
.AddPolicy("MinimumAge", p => p.Requirements.Add(new MinimumAgeRequirement(18)));
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
// ── Custom Policy Handler ──
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge) => MinimumAge = minimumAge;
}
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
{
var dob = context.User.FindFirst(c => c.Type == "DateOfBirth")?.Value;
if (dob is not null)
{
var age = DateTime.Today.Year - DateTime.Parse(dob).Year;
if (age >= requirement.MinimumAge)
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}UserSecrets for dev, Azure Key Vault or AWS Secrets Manager for production. Use [AllowAnonymous] explicitly for public endpoints — everything is protected by default.| Middleware | Package | Purpose |
|---|---|---|
| UseHttpsRedirection | Built-in | Redirect HTTP to HTTPS |
| UseCors | Built-in | Configure CORS policies |
| UseAuthentication | Built-in | Set HttpContext.User |
| UseAuthorization | Built-in | Enforce [Authorize] |
| UseResponseCaching | Built-in | Cache responses server-side |
| UseResponseCompression | Built-in | Gzip/Brotli compression |
| UseExceptionHandler | Built-in | Global exception handling |
| UseHealthChecks | Microsoft.Extensions.Diagnostics.HealthChecks | Health endpoints |
| UseRateLimiter | Built-in (.NET 7+) | Request rate limiting |
| UseWebSockets | Built-in | WebSocket support |
| Pattern | Description | When to Use |
|---|---|---|
| Inline (lambda) | app.Use(async (ctx, next) => {...}) | Simple, one-off logic |
| Class-based | Class with InvokeAsync(HttpContext) | Reusable, testable |
| Convention-based | Class with specific method signature | Factory pattern, DI support |
// ── Custom Middleware (Class-based) ──
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
public RequestTimingMiddleware(RequestDelegate next,
ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
try
{
await _next(context);
}
finally
{
sw.Stop();
_logger.LogInformation(
"{Method} {Path} → {Status} in {ElapsedMs}ms",
context.Request.Method,
context.Request.Path,
context.Response.StatusCode,
sw.ElapsedMilliseconds);
}
}
}
// ── Extension Method ──
public static class RequestTimingExtensions
{
public static IApplicationBuilder UseRequestTiming(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestTimingMiddleware>();
}
}
// Program.cs
app.UseRequestTiming();app.UseExceptionHandler("/error") as the first middleware. Custom middleware added via extension methods is easier to test and reuse.| Concept | Description |
|---|---|
| Hub | Server-side class — receives/sends messages to clients |
| Connection | Each client gets a unique ConnectionId |
| Groups | Send messages to subset of connections (like chat rooms) |
| Client Methods | Server invokes JS/.NET methods on connected clients |
| Streaming | Server-to-client streaming with IAsyncEnumerable |
| Transports | WebSocket → SSE → Long Polling (auto-negotiation) |
| Transport | Pros | Cons | Support |
|---|---|---|---|
| WebSocket | Lowest latency, full-duplex, least overhead | Requires modern browsers | All modern browsers |
| Server-Sent Events | One-way server → client, auto-reconnect | No client → server | Most browsers |
| Long Polling | Universal fallback | High latency, high overhead | Any browser |
// ── SignalR Hub ──
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public async Task JoinRoom(string room)
{
await Groups.AddToGroupAsync(Context.ConnectionId, room);
await Clients.Group(room).SendAsync("UserJoined", user);
}
public async Task LeaveRoom(string room)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, room);
}
// Server-to-client streaming
public async IAsyncEnumerable<string> StreamData(
[EnumeratorCancellation] CancellationToken ct)
{
for (int i = 0; i < 100 && !ct.IsCancellationRequested; i++)
{
yield return $"Data point {i}";
await Task.Delay(100, ct);
}
}
public override async Task OnConnectedAsync()
{
// Called when client connects
await base.OnConnectedAsync();
}
}
// Program.cs
builder.Services.AddSignalR();
app.MapHub<ChatHub>("/chathub");
// ── JavaScript Client ──
// const connection = new signalR.HubConnectionBuilder()
// .withUrl("/chathub")
// .withAutomaticReconnect()
// .build();
// connection.on("ReceiveMessage", (user, msg) => { ... });
// await connection.start();Groups for rooms/channels. Always handle reconnection in the client. Streaming with IAsyncEnumerable is perfect for real-time dashboards.| Feature | Description |
|---|---|
| Evolution | Successor to Xamarin.Forms — single project, native controls |
| Platforms | iOS, Android, macOS, Windows (from one codebase) |
| Architecture | MVVM, CommunityToolkit.Mvvm recommended |
| UI | XAML or C# markup for declarative UI |
| Handlers | Replace renderers — more performant, lower-level control |
| Shell | Flyout, tabs, URI-based navigation |
| Hot Reload | Edit XAML/C# and see changes instantly |
| Single Project | One .csproj — platform folders auto-handled |
| Framework | Language | UI | Performance |
|---|---|---|---|
| .NET MAUI | C# | Native controls | Near-native |
| React Native | JS/TS | Native bridge | Good |
| Flutter | Dart | Custom renderer | Excellent |
| Xamarin | C# (legacy) | Renderer-based | Good (deprecated) |
| SwiftUI | Swift | Apple native | Best (iOS only) |
| Jetpack Compose | Kotlin | Android native | Best (Android only) |
<!-- ── MAUI XAML Page ── -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MyApp.ViewModels"
x:Class="MyApp.Views.MainPage"
x:DataType="vm:MainViewModel">
<ScrollView>
<VerticalStackLayout Spacing="20" Padding="20">
<Label Text="Product List" FontSize="28" FontAttributes="Bold"/>
<CollectionView ItemsSource="{Binding Products}"
SelectionMode="Single"
SelectedItem="{Binding SelectedProduct}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame Margin="0,0,0,10" Padding="15">
<VerticalStackLayout>
<Label Text="{Binding Name}"
FontSize="18" FontAttributes="Bold"/>
<Label Text="{Binding Price, StringFormat='${0:F2}'}"
TextColor="Green"/>
</VerticalStackLayout>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Button Text="Add Product" Command="{Binding AddCommand}"/>
</VerticalStackLayout>
</ScrollView>
</ContentPage>[ObservableProperty], [RelayCommand] source generators. Handlers replace Xamarin renderers for better performance and flexibility.| Framework | Style | Notes |
|---|---|---|
| xUnit | AAA (Act/Assert/Arrange) | Industry standard for .NET, parallel by default |
| NUnit | Attributes | Rich assertion model, parameterized tests |
| MSTest | Attributes | Visual Studio integration, Azure DevOps |
| FluentAssertions | Extension methods | Readable assertions: result.Should().Be(42) |
| Moq | Mocking library | Setup().Returns(), Verify() |
| NSubstitute | Mocking library | Cleaner API than Moq, no Record/Playback |
| Tool | Purpose |
|---|---|
| WebApplicationFactory | Integration testing — in-memory TestServer |
| Testcontainers | Docker-based integration tests (PostgreSQL, Redis) |
| BenchmarkDotNet | Performance benchmarking (micro-benchmarks) |
| Coverlet | Code coverage (.NET CLI & CI) |
| Shouldly | Fluent assertion library |
| Respawn | Reset database state between tests |
// ── xUnit Unit Test ──
public class ProductServiceTests
{
private readonly Mock<IProductRepository> _repo = new();
private readonly ProductService _service;
public ProductServiceTests()
{
_service = new ProductService(_repo.Object);
}
[Fact]
public async Task GetById_Exists_ReturnsProduct()
{
// Arrange
var expected = new Product { Id = 1, Name = "Test", Price = 10m };
_repo.Setup(r => r.GetByIdAsync(1))
.ReturnsAsync(expected);
// Act
var result = await _service.GetByIdAsync(1);
// Assert
result.Should().NotBeNull();
result.Name.Should().Be("Test");
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-100)]
public void Create_InvalidPrice_ThrowsException(decimal price)
{
var act = () => _service.Create(new CreateProductDto
{
Name = "Test", Price = price
});
act.Should().Throw<ValidationException>();
}
}
// ── Integration Test with WebApplicationFactory ──
public class ApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ApiTests(WebApplicationFactory<Program> factory)
{
_client = factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.ReplaceDbContext<TestDbContext>();
});
})
.CreateClient();
}
[Fact]
public async Task GetProducts_ReturnsOk()
{
var response = await _client.GetAsync("/api/products");
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
}WebApplicationFactory for true integration tests. Use Testcontainers for database integration tests — no shared state between tests.| Aspect | gRPC | REST (HTTP/JSON) |
|---|---|---|
| Protocol | HTTP/2 | HTTP/1.1 or HTTP/2 |
| Format | Protobuf (binary) | JSON (text) |
| Performance | 2-10x faster | Slower serialization |
| Streaming | Bidirectional built-in | Manual (SSE, WebSockets) |
| Contracts | .proto files required | OpenAPI/Swagger optional |
| Browser | Requires gRPC-Web | Native support |
| Code Gen | Auto-generated clients | Manual or NSwag |
| Use Case | Microservices, high-perf | Public APIs, web apps |
| Feature | How |
|---|---|
| Filters | AddEndpointFilter<TFilter>() on route builder |
| Auth | .RequireAuthorization("Policy") on route |
| OpenAPI | .WithOpenApi() — auto-generated OpenAPI docs |
| Groups | .MapGroup("/api/v1") for route prefixing |
| Versioning | .HasApiVersion(1.0) with Asp.Versioning.Http |
| Rate Limiting | .RequireRateLimiting("policy") |
| Output Caching | .CacheOutput(p => p.Expire(TimeSpan.FromMinutes(5))) |
| Typed Results | Results.Ok(), Results.NotFound(), Results.Created() |
// ── gRPC Proto Definition ──
syntax = "proto3";
option csharp_namespace = "MyApp.Grpc";
package greet;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
rpc StreamHellos (HelloRequest) returns (stream HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}// ── gRPC Service Implementation ──
public class GreeterService : Greeter.GreeterBase
{
public override Task<HelloReply> SayHello(
HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = $"Hello, {request.Name}!"
});
}
public override async Task StreamHellos(
HelloRequest request, IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
for (int i = 0; i < 10; i++)
{
await responseStream.WriteAsync(new HelloReply
{
Message = $"Hello #{i}, {request.Name}!"
});
await Task.Delay(500);
}
}
}
// Program.cs
builder.Services.AddGrpc();
app.MapGrpcService<GreeterService>();Minimal APIs + OpenAPI is the recommended pattern for public-facing REST APIs in .NET 9+.| Option | Type | Best For |
|---|---|---|
| BackgroundService | Built-in (long-running) | Queues, polling, recurring tasks |
| IHostedService | Built-in (full lifecycle) | Start/stop control, init tasks |
| Hangfire | Persistent job scheduling | Dashboard, retries, SQL storage |
| Quartz.NET | Cron-based scheduling | Complex schedules, distributed |
| Channel<T> | Producer/consumer pattern | In-memory async queues |
| Concept | Description |
|---|---|
| dotnet new worker | Create Worker Service project template |
| BackgroundService | Base class with ExecuteAsync(CancellationToken) |
| IHostBuilder | Same hosting as ASP.NET Core (DI, config, logging) |
| Graceful shutdown | CancellationToken stops cleanly |
| Standalone or hosted | Can run as Console app or within Web app |
// ── Background Service ──
public class EmailWorker : BackgroundService
{
private readonly IEmailService _emailService;
private readonly ILogger<EmailWorker> _logger;
private readonly Channel<EmailJob> _channel;
public EmailWorker(IEmailService emailService,
ILogger<EmailWorker> logger,
Channel<EmailJob> channel)
{
_emailService = emailService;
_logger = logger;
_channel = channel;
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
await foreach (var job in _channel.Reader.ReadAllAsync(ct))
{
try
{
await _emailService.SendAsync(job.To, job.Subject, job.Body);
_logger.LogInformation("Sent email to {To}", job.To);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send email to {To}", job.To);
}
}
}
}
// Program.cs (register)
builder.Services.AddSingleton(Channel.CreateUnbounded<EmailJob>());
builder.Services.AddHostedService<EmailWorker>();
// ── Channel<T> Producer (in controller/service) ──
public class OrderService
{
private readonly Channel<EmailJob> _channel;
public OrderService(Channel<EmailJob> channel) => _channel = channel;
public async Task PlaceOrderAsync(Order order)
{
// ... save order ...
await _channel.Writer.WriteAsync(new EmailJob
{
To = order.CustomerEmail,
Subject = "Order Confirmed",
Body = $"Your order #{order.Id} is confirmed!"
});
}
}Channel<T> for in-memory producer/consumer — it's lock-free and async-friendly. Use Hangfire for persistent jobs that survive restarts. Use Quartz.NET for cron-based scheduling with distributed locking.| Command | Purpose |
|---|---|
| dotnet new webapi | Create Web API project |
| dotnet new blazor | Create Blazor Web App |
| dotnet new worker | Create Worker Service |
| dotnet new console | Create Console app |
| dotnet build -c Release | Build in Release mode |
| dotnet run | Build and run |
| dotnet test | Run all tests |
| dotnet publish -c Release -o ./publish | Publish for deployment |
| dotnet watch run | Hot reload development |
| dotnet format | Format code (EditorConfig) |
| dotnet ef migrations add Init | Create EF migration |
| dotnet ef database update | Apply migrations |
| Command | Purpose |
|---|---|
| dotnet tool list -g | List installed global tools |
| dotnet tool install -g dotnet-ef | Install EF Core tooling |
| dotnet tool install -g dotnet-aspnet-codegenerator | Code scaffolding |
| dotnet add package Swashbuckle.AspNetCore | Add NuGet package |
| dotnet remove package Microsoft.EntityFrameworkCore | Remove package |
| dotnet list package --outdated | Check for outdated packages |
| dotnet add reference ../Lib/Lib.csproj | Add project reference |
| dotnet new nugetconfig | Create NuGet config |
# ── Common Workflow ──
dotnet new webapi -n MyApi --use-minimal-apis
cd MyApi
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet ef migrations add InitialCreate
dotnet ef database update
dotnet watch run
# ── Publish for Production ──
dotnet publish -c Release -r linux-x64 --self-contained
dotnet publish -c Release -r win-x64 -p:PublishAot=true
# ── Check .NET SDK & Runtime ──
dotnet --list-sdks
dotnet --list-runtimes
dotnet --infodotnet watch during development for hot reload. Use dotnet format before commits for consistent code style. Use dotnet publish -p:PublishAot=true for Native AOT with smallest binary and fastest startup.| Priority | Provider | Notes |
|---|---|---|
| 1 (lowest) | appsettings.json | Default config, checked into source |
| 2 | appsettings.{Environment}.json | Environment-specific override |
| 3 | User Secrets | Dev only — stored in %APPDATA% |
| 4 | Environment variables | Docker/K8s — prefix with __ for nesting |
| 5 | Command-line args | Highest priority — overrides everything |
| Interface | Behavior | Use Case |
|---|---|---|
| IOptions<T> | Singleton, reads once at startup | Static config |
| IOptionsSnapshot<T> | Scoped, reads per-request | Per-request config |
| IOptionsMonitor<T> | Singleton, auto-reloads on change | Dynamic config |
| IOptionsFactory<T> | Creates T instances with DI | Custom creation logic |
// ── appsettings.json ──
// {
// "ConnectionStrings": {
// "Default": "Server=localhost;Database=MyDb;Trusted_Connection=true"
// },
// "Email": {
// "SmtpServer": "smtp.example.com",
// "Port": 587,
// "FromAddress": "noreply@example.com"
// }
// }
// ── Strongly-typed Config ──
public class EmailSettings
{
public string SmtpServer { get; set; } = string.Empty;
public int Port { get; set; }
public string FromAddress { get; set; } = string.Empty;
}
// Program.cs
builder.Services.Configure<EmailSettings>(
builder.Configuration.GetSection("Email"));
builder.Services.AddSingleton(sp =>
sp.GetRequiredService<IOptions<EmailSettings>>().Value);
// ── Serilog Setup ──
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day)
.WriteTo.Seq("http://localhost:5341")
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.CreateLogger();
builder.Host.UseSerilog(); // Use Serilog as the logging provider
// Usage
logger.LogInformation("Processing order {OrderId} for {Customer}", orderId, customer);
logger.LogWarning("Retry {Attempt} of {Max}", attempt, maxRetries);
logger.LogError(exception, "Failed to process payment for {OrderId}", orderId);"User {UserId} logged in from {Ip}" with parameters. This enables structured search in tools like Seq, Elasticsearch, or Application Insights.| Technique | Impact | Use Case |
|---|---|---|
| Native AOT | Startup -60%, Memory -80% | CLI tools, serverless, containers |
| ReadyToRun (R2R) | Startup -30% | General web apps |
| Span<T> / Memory<T> | Zero-alloc slicing | String parsing, buffer processing |
| ArrayPool<T> | Reduce GC pressure | Buffer reuse in hot paths |
| ObjectPool<T> | Reduce allocations | StringBuilder, HttpClient messages |
| ValueTask<T> | Avoid Task allocation | Often-synchronous async results |
| Tool | Purpose | Platform |
|---|---|---|
| dotnet-trace | CPU profiling, GC events | CLI (all platforms) |
| dotnet-dump | Memory dumps, crash analysis | CLI (all platforms) |
| dotnet-counters | Real-time metrics monitoring | CLI (all platforms) |
| BenchmarkDotNet | Micro-benchmarking | Code library |
| Visual Studio Profiler | CPU, memory, async profiling | Windows |
| JetBrains dotTrace | CPU & memory profiling | Windows, Linux, Mac |
// ── Span<T> — Zero-allocation string processing ──
public static ReadOnlySpan<char> GetDomain(ReadOnlySpan<char> email)
{
var atIndex = email.IndexOf('@');
return atIndex >= 0 ? email[(atIndex + 1)..] : ReadOnlySpan<char>.Empty;
}
// Usage (no heap allocation)
var domain = GetDomain("user@example.com".AsSpan());
// ── ArrayPool<T> — Reduce GC pressure ──
public void ProcessLargeData()
{
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(1024 * 1024); // Rent 1MB buffer
try
{
// Use buffer[0..1023*1024]
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
}
finally
{
pool.Return(buffer, clearArray: true); // Return to pool
}
}
// ── ValueTask — Avoid allocation for sync results ──
public ValueTask<int> GetCachedValueAsync(string key)
{
if (_cache.TryGetValue(key, out var value))
return ValueTask.FromResult(value); // No allocation!
return new ValueTask<int>(LoadFromDbAsync(key)); // Allocates only when needed
}
// ── Async best practices ──
// ✅ DO: Use ConfigureAwait(false) in library code
await httpClient.GetStringAsync(url).ConfigureAwait(false);
// ✅ DO: Use CancellationToken
public async Task<List<Product>> GetAllAsync(CancellationToken ct = default)
{
return await db.Products.ToListAsync(ct);
}
// ❌ DON'T: Use Task.Run in ASP.NET Core (already on thread pool)Span<T> and ReadOnlySpan<char> for string processing in hot paths — they create zero allocations. Use ConfigureAwait(false) in library code. Use ValueTask when results are often available synchronously. Always profile before optimizing!| Platform | .NET Support | Use Case |
|---|---|---|
| Azure App Service | First-class, built-in | Web apps, APIs, easiest deployment |
| Azure Functions | First-class | Serverless, event-driven |
| Azure Container Apps | First-class | Serverless containers, KEDA scaling |
| Azure Kubernetes (AKS) | First-class | Container orchestration, microservices |
| AWS Lambda | .NET 8 runtime | Serverless functions |
| AWS ECS / EKS | Docker / K8s | Container orchestration |
| Google Cloud Run | Docker containers | Serverless containers |
| Concept | Description |
|---|---|
| Purpose | Cloud-native app orchestration and observability |
| App Host | Orchestrator — defines services, connections, volumes |
| Service Defaults | Shared config (health checks, OpenTelemetry, retries) |
| Dashboard | Built-in observability — logs, traces, metrics UI |
| Components | Pre-built integrations (PostgreSQL, Redis, RabbitMQ, Azure) |
| Publish | Generate docker-compose or Kubernetes manifests |
# ── .NET Dockerfile (Multi-stage) ──
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["MyApi.csproj", "./"]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApi.dll"]// ── .NET Aspire App Host ──
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.MyApi>("api")
.WithReplicas(3);
var postgres = builder.AddPostgres("postgres")
.AddDatabase("mydb");
var redis = builder.AddRedis("cache");
api.WithReference(postgres)
.WithReference(redis);
builder.AddNpmApp("frontend", "../frontend")
.WithReference(api);
builder.Build().Run();# ── GitHub Actions CI/CD ──
name: Build & Deploy
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- run: dotnet restore
- run: dotnet build -c Release --no-restore
- run: dotnet test -c Release --no-build --verbosity normal
- run: dotnet publish -c Release -o ./publish
- uses: azure/webapps-deploy@v3
with:
app-name: my-api
publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}
package: ./publishdotnet publish + Docker + Azure App Service is the fastest path to production.