.NET Development Skill
Comprehensive foundational patterns for .NET development, covering runtime fundamentals, project structure, dependency injection, configuration management, logging, middleware, metaprogramming, and cross-platform development.
Table of Contents
- .NET Runtime and SDK
- Project Structure
- CLI Tools
- Project Types
- Dependency Injection
- Configuration
- Logging
- Middleware Patterns
- NuGet Packages
- Cross-Platform Development
- Metaprogramming
- Best Practices
- Cross-Cutting Patterns
.NET Runtime and SDK
Runtime Components
The .NET runtime provides the execution environment for .NET applications.
// Runtime information
using System;
using System.Runtime.InteropServices;
public class RuntimeInfo
{
public static void DisplayRuntimeInfo()
{
// Framework description
Console.WriteLine($"Framework: {RuntimeInformation.FrameworkDescription}");
// OS description
Console.WriteLine($"OS: {RuntimeInformation.OSDescription}");
// Architecture
Console.WriteLine($"Architecture: {RuntimeInformation.OSArchitecture}");
// Process architecture
Console.WriteLine($"Process: {RuntimeInformation.ProcessArchitecture}");
// Runtime identifier
Console.WriteLine($"RID: {RuntimeInformation.RuntimeIdentifier}");
}
}
SDK vs Runtime
# Check installed SDKs
dotnet --list-sdks
# Check installed runtimes
dotnet --list-runtimes
# Check SDK version
dotnet --version
# Display all info
dotnet --info
Global.json Configuration
{
"sdk": {
"version": "8.0.0",
"rollForward": "latestMinor",
"allowPrerelease": false
}
}
# Create global.json with specific SDK version
dotnet new globaljson --sdk-version 8.0.100
# Roll forward policies:
# - patch: Use latest patch version
# - feature: Use latest feature band
# - minor: Use latest minor version
# - major: Use latest major version
# - latestPatch: Use latest patch (default)
# - latestMinor: Use latest minor
# - latestMajor: Use latest major
# - disable: Use exact version
.NET Standard vs .NET
// Target Framework Monikers (TFM)
// Modern .NET (cross-platform)
// <TargetFramework>net8.0</TargetFramework>
// <TargetFramework>net7.0</TargetFramework>
// <TargetFramework>net6.0</TargetFramework>
// .NET Standard (compatibility layer)
// <TargetFramework>netstandard2.1</TargetFramework>
// <TargetFramework>netstandard2.0</TargetFramework>
// .NET Framework (Windows only)
// <TargetFramework>net48</TargetFramework>
// <TargetFramework>net472</TargetFramework>
// Multi-targeting
// <TargetFrameworks>net8.0;net6.0;netstandard2.0</TargetFrameworks>
// Conditional compilation
#if NET8_0_OR_GREATER
// Code for .NET 8+
#elif NET6_0_OR_GREATER
// Code for .NET 6+
#elif NETSTANDARD2_0
// Code for .NET Standard 2.0
#endif
Runtime Configuration
// runtimeconfig.json
{
"runtimeOptions": {
"tfm": "net8.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
"configProperties": {
"System.GC.Server": true,
"System.GC.Concurrent": true,
"System.GC.RetainVM": true,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}
Project Structure
Project File (.csproj)
<Project Sdk="Microsoft.NET.Sdk">
<!-- Basic Properties -->
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<RootNamespace>MyCompany.MyProduct</RootNamespace>
<AssemblyName>MyProduct</AssemblyName>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<!-- Package Properties -->
<PropertyGroup>
<PackageId>MyCompany.MyProduct</PackageId>
<Version>1.0.0</Version>
<Authors>Your Name</Authors>
<Company>MyCompany</Company>
<Product>MyProduct</Product>
<Description>Package description</Description>
<Copyright>Copyright © 2024</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/mycompany/myproduct</PackageProjectUrl>
<RepositoryUrl>https://github.com/mycompany/myproduct</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>tag1;tag2;tag3</PackageTags>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
</PropertyGroup>
<!-- Build Properties -->
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<NoWarn>CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<!-- Debug Configuration -->
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
<Optimize>false</Optimize>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<!-- Release Configuration -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
<Optimize>true</Optimize>
<DefineConstants>TRACE</DefineConstants>
</PropertyGroup>
<!-- Package References -->
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
</ItemGroup>
<!-- Project References -->
<ItemGroup>
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
</ItemGroup>
<!-- Conditional Package References -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="System.Text.Json" Version="8.0.0" />
</ItemGroup>
<!-- Content Files -->
<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="appsettings.*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<DependentUpon>appsettings.json</DependentUpon>
</Content>
</ItemGroup>
<!-- Embedded Resources -->
<ItemGroup>
<EmbeddedResource Include="Resources\*.txt" />
</ItemGroup>
<!-- None Items -->
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
<None Include="LICENSE" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>
Solution File (.sln)
# Create a new solution
dotnet new sln -n MySolution
# Add projects to solution
dotnet sln add src/MyApp/MyApp.csproj
dotnet sln add src/MyLibrary/MyLibrary.csproj
dotnet sln add tests/MyApp.Tests/MyApp.Tests.csproj
# List projects in solution
dotnet sln list
# Remove project from solution
dotnet sln remove src/OldProject/OldProject.csproj
Directory Structure
MySolution/
├── .gitignore
├── .editorconfig
├── global.json
├── MySolution.sln
├── Directory.Build.props # Shared properties for all projects
├── Directory.Build.targets # Shared targets for all projects
├── Directory.Packages.props # Central package management
├── src/
│ ├── MyApp/
│ │ ├── MyApp.csproj
│ │ ├── Program.cs
│ │ ├── appsettings.json
│ │ ├── appsettings.Development.json
│ │ └── Controllers/
│ │ └── WeatherController.cs
│ └── MyLibrary/
│ ├── MyLibrary.csproj
│ ├── Class1.cs
│ └── Interfaces/
│ └── IMyService.cs
├── tests/
│ ├── MyApp.Tests/
│ │ ├── MyApp.Tests.csproj
│ │ └── UnitTest1.cs
│ └── MyLibrary.Tests/
│ ├── MyLibrary.Tests.csproj
│ └── Class1Tests.cs
├── docs/
│ └── README.md
└── tools/
└── build.ps1
Directory.Build.props
<Project>
<!-- Common properties for all projects -->
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Version properties -->
<PropertyGroup>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix Condition="'$(Configuration)' == 'Debug'">dev</VersionSuffix>
</PropertyGroup>
<!-- Source Link (for debugging) -->
<PropertyGroup>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
</ItemGroup>
</Project>
Directory.Packages.props (Central Package Management)
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<!-- Common packages -->
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<!-- Test packages -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageVersion Include="xunit" Version="2.6.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.4" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
</Project>
CLI Tools
dotnet new
# List available templates
dotnet new list
# Common templates
dotnet new console -n MyConsoleApp
dotnet new classlib -n MyLibrary
dotnet new web -n MyWebApp
dotnet new webapi -n MyApi
dotnet new mvc -n MyMvcApp
dotnet new razor -n MyRazorApp
dotnet new blazorserver -n MyBlazorApp
dotnet new blazorwasm -n MyBlazorWasmApp
dotnet new worker -n MyWorkerService
dotnet new xunit -n MyTests
dotnet new nunit -n MyNUnitTests
dotnet new mstest -n MyMSTests
# Create with options
dotnet new console -n MyApp -f net8.0 -o src/MyApp
dotnet new webapi -n MyApi --use-controllers --auth Individual
# Install templates
dotnet new install Amazon.Lambda.Templates
dotnet new install Microsoft.AspNetCore.SpaTemplates
# Uninstall templates
dotnet new uninstall Amazon.Lambda.Templates
# Create from template with language
dotnet new console -n MyApp -lang "F#"
dotnet new classlib -n MyLib -lang "VB"
dotnet build
# Build project
dotnet build
# Build with configuration
dotnet build -c Release
dotnet build --configuration Debug
# Build specific framework
dotnet build -f net8.0
# Build with no dependencies
dotnet build --no-dependencies
# Build with no restore
dotnet build --no-restore
# Build with specific runtime
dotnet build -r win-x64
dotnet build -r linux-x64
dotnet build -r osx-x64
dotnet build -r osx-arm64
# Build verbosity
dotnet build -v quiet
dotnet build -v minimal
dotnet build -v normal
dotnet build -v detailed
dotnet build -v diagnostic
# Build and output to directory
dotnet build -o ./output
dotnet run
# Run project
dotnet run
# Run with configuration
dotnet run -c Release
# Run with framework
dotnet run -f net8.0
# Run with arguments
dotnet run -- arg1 arg2
dotnet run --project ./src/MyApp/MyApp.csproj
# Run with environment
dotnet run --environment Production
ASPNETCORE_ENVIRONMENT=Production dotnet run
# Run without build
dotnet run --no-build
# Run without restore
dotnet run --no-restore
# Run with launch profile
dotnet run --launch-profile "Production"
dotnet publish
# Publish for deployment
dotnet publish
# Publish with configuration
dotnet publish -c Release
# Publish with runtime (self-contained)
dotnet publish -r win-x64 --self-contained
dotnet publish -r linux-x64 -p:PublishSingleFile=true
dotnet publish -r osx-arm64 --self-contained
# Publish framework-dependent
dotnet publish -r win-x64 --self-contained false
# Publish with trimming
dotnet publish -c Release -r linux-x64 --self-contained -p:PublishTrimmed=true
dotnet publish -c Release -p:PublishTrimmed=true -p:TrimMode=full
# Publish as single file
dotnet publish -r win-x64 -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfContained=true
# Publish ReadyToRun
dotnet publish -c Release -r win-x64 -p:PublishReadyToRun=true
# Publish to folder
dotnet publish -o ./publish
# Publish with version
dotnet publish -p:Version=1.2.3
dotnet restore
# Restore dependencies
dotnet restore
# Restore with specific source
dotnet restore --source https://api.nuget.org/v3/index.json
dotnet restore -s ./local-packages
# Restore for specific runtime
dotnet restore -r win-x64
# Restore with locked mode (use packages.lock.json)
dotnet restore --locked-mode
# Force evaluation of all dependencies
dotnet restore --force
# Restore without cache
dotnet restore --no-cache
dotnet test
# Run tests
dotnet test
# Run tests with configuration
dotnet test -c Release
# Run tests with verbosity
dotnet test -v normal
dotnet test --verbosity detailed
# Run tests with filter
dotnet test --filter FullyQualifiedName~MyNamespace
dotnet test --filter Category=Unit
dotnet test --filter "Priority=1|Category=Smoke"
# Run tests with coverage
dotnet test --collect:"XPlat Code Coverage"
dotnet test /p:CollectCoverage=true
# Run tests with logger
dotnet test --logger "trx;LogFileName=test-results.trx"
dotnet test --logger "html;LogFileName=test-results.html"
# Run tests without build
dotnet test --no-build
dotnet clean
# Clean build outputs
dotnet clean
# Clean with configuration
dotnet clean -c Release
# Clean specific framework
dotnet clean -f net8.0
# Clean and remove obj folder
dotnet clean
rm -rf **/obj
dotnet pack
# Create NuGet package
dotnet pack
# Pack with configuration
dotnet pack -c Release
# Pack with version
dotnet pack -p:Version=1.2.3
# Pack without build
dotnet pack --no-build
# Pack to output directory
dotnet pack -o ./packages
# Include symbols
dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg
dotnet add/remove
# Add package
dotnet add package Microsoft.Extensions.Logging
dotnet add package Newtonsoft.Json --version 13.0.3
# Add package from source
dotnet add package MyPackage --source ./local-packages
# Add package to specific project
dotnet add src/MyApp/MyApp.csproj package Serilog
# Remove package
dotnet remove package Newtonsoft.Json
# Add project reference
dotnet add reference ../MyLibrary/MyLibrary.csproj
# Remove project reference
dotnet remove reference ../MyLibrary/MyLibrary.csproj
dotnet list
# List packages
dotnet list package
dotnet list package --outdated
dotnet list package --deprecated
dotnet list package --vulnerable
# List project references
dotnet list reference
# List projects in solution
dotnet sln list
dotnet tool
# Install global tool
dotnet tool install -g dotnet-ef
dotnet tool install -g dotnet-format
# Install local tool
dotnet new tool-manifest
dotnet tool install dotnet-ef
# List tools
dotnet tool list -g
dotnet tool list
# Update tool
dotnet tool update -g dotnet-ef
dotnet tool update dotnet-ef
# Uninstall tool
dotnet tool uninstall -g dotnet-ef
dotnet tool uninstall dotnet-ef
# Run tool
dotnet ef --version
dotnet format
Project Types
Console Application
// Program.cs (Top-level statements)
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var builder = Host.CreateApplicationBuilder(args);
// Configure services
builder.Services.AddTransient<IMyService, MyService>();
builder.Services.AddLogging(logging =>
{
logging.AddConsole();
logging.AddDebug();
});
var host = builder.Build();
// Run application
var service = host.Services.GetRequiredService<IMyService>();
await service.RunAsync();
await host.RunAsync();
// Service implementation
public interface IMyService
{
Task RunAsync();
}
public class MyService : IMyService
{
private readonly ILogger<MyService> _logger;
public MyService(ILogger<MyService> logger)
{
_logger = logger;
}
public async Task RunAsync()
{
_logger.LogInformation("Service started at {Time}", DateTime.UtcNow);
await Task.Delay(1000);
_logger.LogInformation("Service completed at {Time}", DateTime.UtcNow);
}
}
Class Library
// MyLibrary.csproj
/*
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
*/
namespace MyLibrary;
/// <summary>
/// Calculator service for basic arithmetic operations.
/// </summary>
public class Calculator
{
/// <summary>
/// Adds two numbers.
/// </summary>
/// <param name="a">First number</param>
/// <param name="b">Second number</param>
/// <returns>Sum of a and b</returns>
public int Add(int a, int b) => a + b;
/// <summary>
/// Subtracts two numbers.
/// </summary>
/// <param name="a">First number</param>
/// <param name="b">Second number</param>
/// <returns>Difference of a and b</returns>
public int Subtract(int a, int b) => a - b;
}
Web API
// Program.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// CORS
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseCors();
app.UseAuthorization();
app.MapControllers();
app.Run();
// Controllers/WeatherController.cs
using Microsoft.AspNetCore.Mvc;
namespace MyApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class WeatherController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherController> _logger;
public WeatherController(ILogger<WeatherController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
_logger.LogInformation("Getting weather forecast");
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
[HttpGet("{id}")]
public ActionResult<WeatherForecast> GetById(int id)
{
if (id < 1 || id > 5)
return NotFound();
return new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(id)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
};
}
[HttpPost]
public ActionResult<WeatherForecast> Create(WeatherForecast forecast)
{
_logger.LogInformation("Creating weather forecast");
return CreatedAtAction(nameof(GetById), new { id = 1 }, forecast);
}
}
public record WeatherForecast
{
public DateOnly Date { get; init; }
public int TemperatureC { get; init; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; init; }
}
Worker Service
// Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
await host.RunAsync();
// Worker.cs
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(10000, stoppingToken);
}
}
public override async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Worker starting");
await base.StartAsync(cancellationToken);
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Worker stopping");
await base.StopAsync(cancellationToken);
}
}
Dependency Injection
Service Lifetimes
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// Transient: Created each time requested
builder.Services.AddTransient<ITransientService, TransientService>();
// Scoped: Created once per scope (request in web apps)
builder.Services.AddScoped<IScopedService, ScopedService>();
// Singleton: Created once for application lifetime
builder.Services.AddSingleton<ISingletonService, SingletonService>();
// Singleton instance
builder.Services.AddSingleton<IConfigService>(new ConfigService("config"));
// Singleton factory
builder.Services.AddSingleton<IFactoryService>(sp =>
{
var logger = sp.GetRequiredService<ILogger<FactoryService>>();
return new FactoryService(logger);
});
var host = builder.Build();
Service Registration Patterns
using Microsoft.Extensions.DependencyInjection;
public static class ServiceCollectionExtensions
{
// Extension method for clean registration
public static IServiceCollection AddMyServices(
this IServiceCollection services)
{
services.AddTransient<IMyService, MyService>();
services.AddScoped<IDataService, DataService>();
services.AddSingleton<ICacheService, CacheService>();
return services;
}
// Generic registration
public static IServiceCollection AddRepository<TEntity>(
this IServiceCollection services)
where TEntity : class
{
services.AddScoped<IRepository<TEntity>, Repository<TEntity>>();
return services;
}
// Conditional registration
public static IServiceCollection AddConditionalService(
this IServiceCollection services,
bool condition)
{
if (condition)
{
services.AddTransient<IService, ServiceA>();
}
else
{
services.AddTransient<IService, ServiceB>();
}
return services;
}
// TryAdd - only adds if not already registered
public static IServiceCollection AddDefaultServices(
this IServiceCollection services)
{
services.TryAddTransient<IMyService, MyService>();
services.TryAddScoped<IDataService, DataService>();
services.TryAddSingleton<ICacheService, CacheService>();
return services;
}
// Replace service
public static IServiceCollection ReplaceService(
this IServiceCollection services)
{
services.Replace(ServiceDescriptor.Transient<IMyService, NewMyService>());
return services;
}
// Remove service
public static IServiceCollection RemoveService(
this IServiceCollection services)
{
var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IMyService));
if (descriptor != null)
{
services.Remove(descriptor);
}
return services;
}
}
Constructor Injection
public interface IEmailService
{
Task SendEmailAsync(string to, string subject, string body);
}
public interface ILogger<T>
{
void LogInformation(string message);
}
public class UserService
{
private readonly IEmailService _emailService;
private readonly ILogger<UserService> _logger;
private readonly IConfiguration _configuration;
// Constructor injection
public UserService(
IEmailService emailService,
ILogger<UserService> logger,
IConfiguration configuration)
{
_emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
public async Task CreateUserAsync(string email, string name)
{
_logger.LogInformation("Creating user {Name}", name);
var welcomeMessage = _configuration["Messages:Welcome"] ?? "Welcome!";
await _emailService.SendEmailAsync(email, "Welcome", welcomeMessage);
}
}
Service Resolution
using Microsoft.Extensions.DependencyInjection;
public class ServiceResolver
{
private readonly IServiceProvider _serviceProvider;
public ServiceResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void ResolveServices()
{
// Get required service (throws if not found)
var requiredService = _serviceProvider.GetRequiredService<IMyService>();
// Get service (returns null if not found)
var optionalService = _serviceProvider.GetService<IOptionalService>();
// Get all implementations
var allServices = _serviceProvider.GetServices<IMyService>();
// Create scope
using var scope = _serviceProvider.CreateScope();
var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();
// Resolve with keyed services (.NET 8+)
var keyedService = _serviceProvider.GetRequiredKeyedService<ICache>("redis");
var anotherKeyedService = _serviceProvider.GetKeyedService<ICache>("memory");
}
}
// Keyed services (.NET 8+)
public static class KeyedServiceExtensions
{
public static IServiceCollection AddKeyedServices(
this IServiceCollection services)
{
services.AddKeyedSingleton<ICache, RedisCache>("redis");
services.AddKeyedSingleton<ICache, MemoryCache>("memory");
return services;
}
}
public class ServiceUsingKeyedDependencies
{
private readonly ICache _redisCache;
private readonly ICache _memoryCache;
public ServiceUsingKeyedDependencies(
[FromKeyedServices("redis")] ICache redisCache,
[FromKeyedServices("memory")] ICache memoryCache)
{
_redisCache = redisCache;
_memoryCache = memoryCache;
}
}
Configuration
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=mydb;User Id=sa;Password=P@ssw0rd;",
"Redis": "localhost:6379"
},
"AppSettings": {
"ApplicationName": "My Application",
"Version": "1.0.0",
"Features": {
"EnableFeatureA": true,
"EnableFeatureB": false
}
},
"Email": {
"SmtpServer": "smtp.example.com",
"SmtpPort": 587,
"FromAddress": "noreply@example.com",
"EnableSsl": true
},
"Cache": {
"SlidingExpiration": "00:30:00",
"AbsoluteExpiration": "01:00:00"
},
"ApiClients": {
"ExternalApi": {
"BaseUrl": "https://api.example.com",
"Timeout": "00:00:30",
"ApiKey": ""
}
}
}
Environment-Specific Configuration
// appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Debug"
}
},
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=mydb_dev;User Id=dev;Password=dev123;"
}
}
// appsettings.Production.json
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Error"
}
}
}
// appsettings.Staging.json
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
Configuration Options Pattern
// AppSettings.cs
public class AppSettings
{
public const string SectionName = "AppSettings";
public string ApplicationName { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public FeatureSettings Features { get; set; } = new();
}
public class FeatureSettings
{
public bool EnableFeatureA { get; set; }
public bool EnableFeatureB { get; set; }
}
public class EmailSettings
{
public const string SectionName = "Email";
public string SmtpServer { get; set; } = string.Empty;
public int SmtpPort { get; set; }
public string FromAddress { get; set; } = string.Empty;
public bool EnableSsl { get; set; }
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Bind configuration sections to strongly-typed options
builder.Services.Configure<AppSettings>(
builder.Configuration.GetSection(AppSettings.SectionName));
builder.Services.Configure<EmailSettings>(
builder.Configuration.GetSection(EmailSettings.SectionName));
// Using IOptions<T>
public class MyService
{
private readonly AppSettings _appSettings;
private readonly EmailSettings _emailSettings;
public MyService(
IOptions<AppSettings> appOptions,
IOptions<EmailSettings> emailOptions)
{
_appSettings = appOptions.Value;
_emailSettings = emailOptions.Value;
}
public void DoSomething()
{
Console.WriteLine(_appSettings.ApplicationName);
Console.WriteLine(_emailSettings.SmtpServer);
}
}
// Using IOptionsSnapshot<T> (reloads on change, scoped)
public class MyReloadableService
{
private readonly IOptionsSnapshot<AppSettings> _appSettings;
public MyReloadableService(IOptionsSnapshot<AppSettings> appSettings)
{
_appSettings = appSettings;
}
public void DoSomething()
{
// Gets current value (reloaded if config changed)
Console.WriteLine(_appSettings.Value.ApplicationName);
}
}
// Using IOptionsMonitor<T> (reloads on change, singleton)
public class MyMonitoredService
{
private readonly IOptionsMonitor<AppSettings> _appSettings;
public MyMonitoredService(IOptionsMonitor<AppSettings> appSettings)
{
_appSettings = appSettings;
// Subscribe to changes
_appSettings.OnChange(settings =>
{
Console.WriteLine("Configuration changed!");
});
}
public void DoSomething()
{
Console.WriteLine(_appSettings.CurrentValue.ApplicationName);
}
}
Configuration Sources
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
// Clear default sources
builder.Configuration.Sources.Clear();
// Add configuration sources in order (later sources override earlier ones)
builder.Configuration
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
.AddXmlFile("config.xml", optional: true, reloadOnChange: true)
.AddIniFile("config.ini", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.AddEnvironmentVariables(prefix: "MYAPP_")
.AddCommandLine(args)
.AddUserSecrets<Program>() // Development only
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Key1"] = "Value1",
["Key2"] = "Value2"
});
var host = builder.Build();
User Secrets (Development)
# Initialize user secrets
dotnet user-secrets init
# Set secret
dotnet user-secrets set "ApiKey" "my-secret-key"
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost;Database=mydb;"
# List secrets
dotnet user-secrets list
# Remove secret
dotnet user-secrets remove "ApiKey"
# Clear all secrets
dotnet user-secrets clear
Environment Variables
# Windows (PowerShell)
$env:ASPNETCORE_ENVIRONMENT = "Development"
$env:ConnectionStrings__DefaultConnection = "Server=localhost;Database=mydb;"
$env:AppSettings__ApplicationName = "My App"
# Linux/macOS
export ASPNETCORE_ENVIRONMENT=Development
export ConnectionStrings__DefaultConnection="Server=localhost;Database=mydb;"
export AppSettings__ApplicationName="My App"
# Note: __ (double underscore) represents nested configuration sections
# AppSettings__Features__EnableFeatureA maps to AppSettings:Features:EnableFeatureA
Logging
ILogger Interface
using Microsoft.Extensions.Logging;
public class MyService
{
private readonly ILogger<MyService> _logger;
public MyService(ILogger<MyService> logger)
{
_logger = logger;
}
public void DoWork()
{
// Log levels
_logger.LogTrace("Trace message");
_logger.LogDebug("Debug message");
_logger.LogInformation("Information message");
_logger.LogWarning("Warning message");
_logger.LogError("Error message");
_logger.LogCritical("Critical message");
// Structured logging with parameters
var userId = 123;
var userName = "John Doe";
_logger.LogInformation("User {UserId} logged in: {UserName}", userId, userName);
// Exception logging
try
{
throw new InvalidOperationException("Something went wrong");
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while processing user {UserId}", userId);
}
// Log with event ID
_logger.LogInformation(new EventId(1001, "UserLogin"),
"User {UserId} logged in", userId);
// Conditional logging
if (_logger.IsEnabled(LogLevel.Debug))
{
var expensiveData = GetExpensiveDebugData();
_logger.LogDebug("Debug data: {Data}", expensiveData);
}
// Log scopes
using (_logger.BeginScope("Processing user {UserId}", userId))
{
_logger.LogInformation("Starting process");
_logger.LogInformation("Process step 1");
_logger.LogInformation("Process step 2");
_logger.LogInformation("Completed process");
}
}
private string GetExpensiveDebugData() => "expensive data";
}
Configure Logging
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.ClearProviders(); // Remove default providers
// Add providers
builder.Logging.AddConsole();
builder.Logging.AddDebug();
builder.Logging.AddEventSourceLogger();
// Set minimum level
builder.Logging.SetMinimumLevel(LogLevel.Information);
// Add filtering
builder.Logging.AddFilter("Microsoft", LogLevel.Warning);
builder.Logging.AddFilter("System", LogLevel.Warning);
builder.Logging.AddFilter("MyApp", LogLevel.Debug);
// Configure from appsettings.json
builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
var host = builder.Build();
Logging Configuration in appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore": "Warning",
"MyApp": "Debug"
},
"Console": {
"IncludeScopes": true,
"LogLevel": {
"Default": "Information"
}
},
"Debug": {
"LogLevel": {
"Default": "Debug"
}
}
}
}
Third-Party Logging (Serilog Example)
// Install packages:
// dotnet add package Serilog.AspNetCore
// dotnet add package Serilog.Sinks.Console
// dotnet add package Serilog.Sinks.File
// dotnet add package Serilog.Sinks.Seq
using Serilog;
var builder = WebApplication.CreateBuilder(args);
// Configure Serilog
builder.Host.UseSerilog((context, configuration) =>
{
configuration
.ReadFrom.Configuration(context.Configuration)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithThreadId()
.WriteTo.Console()
.WriteTo.File(
path: "logs/log-.txt",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30)
.WriteTo.Seq("http://localhost:5341");
});
var app = builder.Build();
// Add Serilog request logging
app.UseSerilogRequestLogging();
app.Run();
Middleware Patterns
Built-in Middleware
using Microsoft.AspNetCore.Builder;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Order matters! Middleware executes in order added
// Exception handling (should be first)
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// HTTPS redirection
app.UseHttpsRedirection();
// Static files
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "StaticFiles")),
RequestPath = "/files"
});
// Routing
app.UseRouting();
// CORS (must be after routing, before authorization)
app.UseCors();
// Authentication
app.UseAuthentication();
// Authorization (must be after authentication)
app.UseAuthorization();
// Response caching
app.UseResponseCaching();
// Response compression
app.UseResponseCompression();
// Session
app.UseSession();
// Endpoints (should be last)
app.MapControllers();
app.MapRazorPages();
app.Run();
Custom Middleware (Inline)
var app = builder.Build();
// Use method
app.Use(async (context, next) =>
{
// Before next middleware
Console.WriteLine($"Before: {context.Request.Path}");
await next(context);
// After next middleware
Console.WriteLine($"After: {context.Response.StatusCode}");
});
// Run method (terminal middleware)
app.Run(async context =>
{
await context.Response.WriteAsync("Hello World!");
});
// Map method (branch pipeline)
app.Map("/api", apiApp =>
{
apiApp.Use(async (context, next) =>
{
Console.WriteLine("API middleware");
await next(context);
});
apiApp.Run(async context =>
{
await context.Response.WriteAsync("API Response");
});
});
// MapWhen method (conditional branch)
app.MapWhen(
context => context.Request.Query.ContainsKey("branch"),
branchApp =>
{
branchApp.Run(async context =>
{
await context.Response.WriteAsync("Branch pipeline");
});
});
app.Run();
Custom Middleware Class
// RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestL
…(truncated)