| name | lang-dotnet-dev |
| description | Foundational .NET patterns covering runtime, project structure, dependency injection, configuration, metaprogramming, and cross-platform development. Use when working with .NET projects or CLI tools. This is the entry point for .NET development. |
.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 RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var startTime = DateTime.UtcNow;
_logger.LogInformation(
"Incoming request: {Method} {Path}",
context.Request.Method,
context.Request.Path);
try
{
await _next(context);
}
finally
{
var elapsed = DateTime.UtcNow - startTime;
_logger.LogInformation(
"Completed request: {Method} {Path} - Status: {StatusCode} - Duration: {Duration}ms",
context.Request.Method,
context.Request.Path,
context.Response.StatusCode,
elapsed.TotalMilliseconds);
}
}
}
// Extension method for registration
public static class RequestLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
// Usage in Program.cs
var app = builder.Build();
app.UseRequestLogging();
app.Run();
Exception Handling Middleware
public class GlobalExceptionHandlerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
public GlobalExceptionHandlerMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionHandlerMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception occurred");
await HandleExceptionAsync(context, ex);
}
}
private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = exception switch
{
ArgumentException => StatusCodes.Status400BadRequest,
UnauthorizedAccessException => StatusCodes.Status401Unauthorized,
KeyNotFoundException => StatusCodes.Status404NotFound,
_ => StatusCodes.Status500InternalServerError
};
var response = new
{
statusCode = context.Response.StatusCode,
message = exception.Message,
detailed = exception.ToString() // Only in development!
};
await context.Response.WriteAsJsonAsync(response);
}
}
NuGet Packages
Common Packages
# Core Microsoft packages
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Logging
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Options
# ASP.NET Core
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.AspNetCore.Mvc.NewtonsoftJson
dotnet add package Swashbuckle.AspNetCore
# Entity Framework Core
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
# Testing
dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Microsoft.NET.Test.Sdk
dotnet add package Moq
dotnet add package FluentAssertions
dotnet add package coverlet.collector
# Serialization
dotnet add package System.Text.Json
dotnet add package Newtonsoft.Json
# HTTP
dotnet add package Microsoft.Extensions.Http
dotnet add package Microsoft.Extensions.Http.Polly
dotnet add package Polly
# Validation
dotnet add package FluentValidation
dotnet add package FluentValidation.AspNetCore
# Mapping
dotnet add package AutoMapper
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
# Logging
dotnet add package Serilog
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
# Authentication
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
Package Management
# List installed packages
dotnet list package
# List outdated packages
dotnet list package --outdated
# List deprecated packages
dotnet list package --deprecated
# List vulnerable packages
dotnet list package --vulnerable
# Update package
dotnet add package PackageName --version 2.0.0
# Remove package
dotnet remove package PackageName
# Restore packages
dotnet restore
# Clear NuGet cache
dotnet nuget locals all --clear
Creating NuGet Packages
<!-- MyLibrary.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<!-- Package metadata -->
<PackageId>MyCompany.MyLibrary</PackageId>
<Version>1.0.0</Version>
<Authors>Your Name</Authors>
<Company>MyCompany</Company>
<Description>Library description here</Description>
<PackageTags>library;utility;helpers</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/mycompany/mylibrary</PackageProjectUrl>
<RepositoryUrl>https://github.com/mycompany/mylibrary</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>
<!-- Include symbols -->
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>
# Create package
dotnet pack -c Release
# Create package with version
dotnet pack -c Release -p:PackageVersion=1.0.1
# Publish to NuGet.org
dotnet nuget push bin/Release/MyLibrary.1.0.0.nupkg --api-key YOUR_API_KEY --source https://api.nuget.org/v3/index.json
# Publish to private feed
dotnet nuget push bin/Release/MyLibrary.1.0.0.nupkg --source http://myfeed.example.com/nuget
Cross-Platform Development
Runtime Identifiers (RIDs)
# Common RIDs
# Windows
win-x64 # Windows 64-bit
win-x86 # Windows 32-bit
win-arm64 # Windows ARM64
# Linux
linux-x64 # Linux 64-bit
linux-arm # Linux ARM
linux-arm64 # Linux ARM64
linux-musl-x64 # Alpine Linux
# macOS
osx-x64 # macOS Intel
osx-arm64 # macOS Apple Silicon
# Publish for specific runtime
dotnet publish -r win-x64 --self-contained
dotnet publish -r linux-x64 --self-contained
dotnet publish -r osx-arm64 --self-contained
Platform-Specific Code
using System.Runtime.InteropServices;
public class PlatformService
{
public string GetPlatformInfo()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return "Running on Windows";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "Running on Linux";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "Running on macOS";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD))
{
return "Running on FreeBSD";
}
return "Unknown platform";
}
public string GetArchitecture()
{
return RuntimeInformation.OSArchitecture switch
{
Architecture.X64 => "x64",
Architecture.X86 => "x86",
Architecture.Arm => "ARM",
Architecture.Arm64 => "ARM64",
_ => "Unknown"
};
}
}
// Conditional compilation
public class PlatformSpecificFeature
{
public void DoSomething()
{
#if WINDOWS
WindowsSpecificCode();
#elif LINUX
LinuxSpecificCode();
#elif OSX
MacOSSpecificCode();
#endif
}
private void WindowsSpecificCode() { }
private void LinuxSpecificCode() { }
private void MacOSSpecificCode() { }
}
Path Handling
using System.IO;
public class PathHelper
{
public void HandlePaths()
{
// Use Path.Combine for cross-platform paths
var configPath = Path.Combine("config", "appsettings.json");
// Get directory separator (\ on Windows, / on Unix)
var separator = Path.DirectorySeparatorChar;
// Get path separator (; on Windows, : on Unix)
var pathSeparator = Path.PathSeparator;
// Get temp path
var tempPath = Path.GetTempPath();
// Get user profile directory
var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
// Get application data directory
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
}
}
Metaprogramming
Reflection
Reflection allows runtime inspection and manipulation of types, methods, and properties.
using System;
using System.Reflection;
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public void Greet() => Console.WriteLine($"Hello, I'm {Name}");
}
public class ReflectionExamples
{
public void InspectType()
{
// Get type information
Type type = typeof(User);
Type type2 = user.GetType();
Type type3 = Type.GetType("Namespace.User");
// Get type properties
Console.WriteLine($"Name: {type.Name}");
Console.WriteLine($"FullName: {type.FullName}");
Console.WriteLine($"Namespace: {type.Namespace}");
Console.WriteLine($"IsClass: {type.IsClass}");
Console.WriteLine($"IsPublic: {type.IsPublic}");
// Get properties
foreach (PropertyInfo prop in type.GetProperties())
{
Console.WriteLine($"{prop.Name}: {prop.PropertyType}");
}
// Get methods
foreach (MethodInfo method in type.GetMethods())
{
Console.WriteLine($"{method.Name}");
}
}
public void CreateInstance()
{
Type type = typeof(User);
// Create instance with parameterless constructor
object instance = Activator.CreateInstance(type);
// Create instance with constructor parameters
object instance2 = Activator.CreateInstance(
type,
new object[] { "Alice", 30 }
);
// Generic version
User user = Activator.CreateInstance<User>();
}
public void AccessMembers()
{
var user = new User { Name = "Alice", Age = 30 };
Type type = typeof(User);
// Get property value
PropertyInfo nameProp = type.GetProperty("Name");
string name = (string)nameProp.GetValue(user);
// Set property value
nameProp.SetValue(user, "Bob");
// Invoke method
MethodInfo greetMethod = type.GetMethod("Greet");
greetMethod.Invoke(user, null);
// Access private members
FieldInfo privateField = type.GetField(
"_privateField",
BindingFlags.NonPublic | BindingFlags.Instance
);
privateField?.SetValue(user, "secret");
}
}
Attributes
Attributes provide metadata about code elements that can be queried at runtime or compile time.
using System;
// Built-in attributes
[Obsolete("Use NewMethod instead")]
public void OldMethod() { }
[Serializable]
public class DataModel { }
[AttributeUsage(AttributeTargets.Method)]
public class LoggableAttribute : Attribute
{
public string Message { get; set; }
public LogLevel Level { get; set; }
public LoggableAttribute(string message = "", LogLevel level = LogLevel.Info)
{
Message = message;
Level = level;
}
}
// Custom attribute with multiple targets
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple = true,
Inherited = true
)]
public class AuthorAttribute : Attribute
{
public string Name { get; }
public string Date { get; set; }
public AuthorAttribute(string name)
{
Name = name;
}
}
// Usage
public class MyService
{
[Loggable("Processing data", LogLevel.Debug)]
[Author("Alice", Date = "2024-01-01")]
public void ProcessData()
{
// Method implementation
}
}
// Reading attributes
public class AttributeReader
{
public void ReadMethodAttributes()
{
var method = typeof(MyService).GetMethod("ProcessData");
// Get single attribute
var loggable = method.GetCustomAttribute<LoggableAttribute>();
if (loggable != null)
{
Console.WriteLine($"Log: {loggable.Message} at {loggable.Level}");
}
// Get all attributes of a type
var authors = method.GetCustomAttributes<AuthorAttribute>();
foreach (var author in authors)
{
Console.WriteLine($"Author: {author.Name} on {author.Date}");
}
// Check if attribute is present
bool isLoggable = method.IsDefined(typeof(LoggableAttribute));
}
}
Source Generators (C# 9+)
Source generators create code at compile time, providing compile-time metaprogramming.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Text;
// Source generator
[Generator]
public class AutoNotifyGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// Register syntax receiver
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is not SyntaxReceiver receiver)
return;
foreach (var field in receiver.CandidateFields)
{
var model = context.Compilation.GetSemanticModel(field.SyntaxTree);
var fieldSymbol = model.GetDeclaredSymbol(field) as IFieldSymbol;
if (fieldSymbol == null)
continue;
var classSymbol = fieldSymbol.ContainingType;
var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
var source = GeneratePropertySource(
namespaceName,
classSymbol.Name,
fieldSymbol.Name
);
context.AddSource(
$"{classSymbol.Name}_{fieldSymbol.Name}_AutoNotify.g.cs",
SourceText.From(source, Encoding.UTF8)
);
}
}
private string GeneratePropertySource(
string namespaceName,
string className,
string fieldName)
{
var propertyName = char.ToUpper(fieldName[1]) + fieldName.Substring(2);
return $@"
using System.ComponentModel;
namespace {namespaceName}
{{
public partial class {className} : INotifyPropertyChanged
{{
public event PropertyChangedEventHandler? PropertyChanged;
public {fieldName.TrimStart('_')} {propertyName}
{{
get => {fieldName};
set
{{
if ({fieldName} != value)
{{
{fieldName} = value;
PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(nameof({propertyName}))
);
}}
}}
}}
}}
}}";
}
class SyntaxReceiver : ISyntaxReceiver
{
public List<FieldDeclarationSyntax> CandidateFields { get; } = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is FieldDeclarationSyntax fieldDeclaration &&
fieldDeclaration.AttributeLists.Count > 0)
{
CandidateFields.Add(fieldDeclaration);
}
}
}
}
// Usage in consuming code
public partial class Person
{
[AutoNotify]
private string _name = "";
// Property is generated by source generator
}
Expression Trees
Expression trees represent code as data structures for runtime code generation and analysis.
using System;
using System.Linq.Expressions;
public class ExpressionTreeExamples
{
public void BasicExpressions()
{
// Lambda expression
Expression<Func<int, int, int>> add = (a, b) => a + b;
// Compile and execute
var compiled = add.Compile();
int result = compiled(2, 3); // 5
// Inspect expression tree
Console.WriteLine(add.Body); // (a + b)
Console.WriteLine(add.Parameters); // [a, b]
}
public void BuildExpressionTree()
{
// Build expression: (a, b) => a + b
var paramA = Expression.Parameter(typeof(int), "a");
var paramB = Expression.Parameter(typeof(int), "b");
var body = Expression.Add(paramA, paramB);
var lambda = Expression.Lambda<Func<int, int, int>>(
body,
paramA,
paramB
);
var compiled = lambda.Compile();
int result = compiled(2, 3); // 5
}
public void ComplexExpressions()
{
// Build: user => user.Name == "Alice" && user.Age > 18
var param = Expression.Parameter(typeof(User), "user");
var nameProperty = Expression.Property(param, "Name");
var nameEquals = Expression.Equal(
nameProperty,
Expression.Constant("Alice")
);
var ageProperty = Expression.Property(param, "Age");
var ageGreater = Expression.GreaterThan(
ageProperty,
Expression.Constant(18)
);
var condition = Expression.AndAlso(nameEquals, ageGreater);
var lambda = Expression.Lambda<Func<User, bool>>(
condition,
param
);
// Use with LINQ
var users = new List<User>();
var filtered = users.AsQueryable().Where(lambda);
}
public void DynamicPropertyAccess()
{
// Build property accessor dynamically
var param = Expression.Parameter(typeof(User), "user");
var property = Expression.Property(param, "Name");
var lambda = Expression.Lambda<Func<User, string>>(property, param);
var accessor = lambda.Compile();
var user = new User { Name = "Alice" };
string name = accessor(user); // "Alice"
}
}
IL Emit (Reflection.Emit)
Generate IL code dynamically at runtime for maximum performance and flexibility.
using System;
using System.Reflection;
using System.Reflection.Emit;
public class ILEmitExamples
{
public void CreateDynamicMethod()
{
// Create dynamic method: int Add(int a, int b) => a + b
var method = new DynamicMethod(
"Add",
typeof(int),
new[] { typeof(int), typeof(int) }
);
ILGenerator il = method.GetILGenerator();
// IL: ldarg.0 (load first argument)
il.Emit(OpCodes.Ldarg_0);
// IL: ldarg.1 (load second argument)
il.Emit(OpCodes.Ldarg_1);
// IL: add (add top two stack values)
il.Emit(OpCodes.Add);
// IL: ret (return)
il.Emit(OpCodes.Ret);
// Create delegate
var add = (Func<int, int, int>)method.CreateDelegate(
typeof(Func<int, int, int>)
);
int result = add(2, 3); // 5
}
public void CreateDynamicType()
{
// Create assembly
var assemblyName = new AssemblyName("DynamicAssembly");
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
assemblyName,
AssemblyBuilderAccess.Run
);
// Create module
var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
// Create type
var typeBuilder = moduleBuilder.DefineType(
"DynamicType",
TypeAttributes.Public
);
// Add field
var fieldBuilder = typeBuilder.DefineField(
"_value",
typeof(int),
FieldAttributes.Private
);
// Add property
var propertyBuilder = typeBuilder.DefineProperty(
"Value",
PropertyAttributes.HasDefault,
typeof(int),
null
);
// Add getter
var getterBuilder = typeBuilder.DefineMethod(
"get_Value",
MethodAttributes.Public | MethodAttributes.SpecialName,
typeof(int),
Type.EmptyTypes
);
var getterIL = getterBuilder.GetILGenerator();
getterIL.Emit(OpCodes.Ldarg_0);
getterIL.Emit(OpCodes.Ldfld, fieldBuilder);
getterIL.Emit(OpCodes.Ret);
propertyBuilder.SetGetMethod(getterBuilder);
// Create type
Type dynamicType = typeBuilder.CreateType();
object instance = Activator.CreateInstance(dynamicType);
}
}
Dynamic Language Runtime (DLR)
The DLR provides dynamic typing and late binding for .NET.
using System;
using System.Dynamic;
public class DynamicExamples
{
public void UseDynamic()
{
// Dynamic variables
dynamic obj = "Hello";
obj = 42; // OK - type can change
obj = new { Name = "Alice" }; // OK - anonymous type
Console.WriteLine(obj.Name); // Late binding
// Dynamic method calls
dynamic calculator = new Calculator();
var result = calculator.Add(2, 3); // Resolved at runtime
}
public void ExpandoObject()
{
// Dynamic object with runtime properties
dynamic person = new ExpandoObject();
person.Name = "Alice";
person.Age = 30;
person.Greet = (Action)(() => Console.WriteLine($"Hello, I'm {person.Name}"));
person.Greet(); // Call dynamic method
// Can enumerate properties
var dict = (IDictionary<string, object>)person;
foreach (var kvp in dict)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
}
}
// Custom dynamic object
public class DynamicDictionary : DynamicObject
{
private readonly Dictionary<string, object> _data = new();
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
return _data.TryGetValue(binder.Name, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
_data[binder.Name] = value;
return true;
}
public override bool TryInvokeMember(
InvokeMemberBinder binder,
object[] args,
out object result)
{
if (_data.TryGetValue(binder.Name, out var value) && value is Delegate del)
{
result = del.DynamicInvoke(args);
return true;
}
result = null;
return false;
}
}
Proxy and Interception Patterns
Use DispatchProxy for dynamic proxy generation.
using System;
using System.Reflection;
public interface IUserService
{
User GetUser(int id);
void SaveUser(User user);
}
public class LoggingProxy<T> : DispatchProxy
{
private T _target;
private ILogger _logger;
public static T Create(T target, ILogger logger)
{
var proxy = Create<T, LoggingProxy<T>>() as LoggingProxy<T>;
proxy._target = target;
proxy._logger = logger;
return (T)(object)proxy;
}
protected override object Invoke(MethodInfo targetMethod, object[] args)
{
_logger.LogInformation(
"Calling {Method} with {Args}",
targetMethod.Name,
args
);
try
{
var result = targetMethod.Invoke(_target, args);
_logger.LogInformation(
"Completed {Method} with result {Result}",
targetMethod.Name,
result
);
return result;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in {Method}",
targetMethod.Name
);
throw;
}
}
}
// Usage
var userService = new UserService();
var logger = new ConsoleLogger();
var proxy = LoggingProxy<IUserService>.Create(userService, logger);
proxy.GetUser(1); // Logged automatically
See also: patterns-metaprogramming-dev for cross-language metaprogramming patterns
Best Practices
Nullable Reference Types
#nullable enable
public class UserService
{
// Non-nullable reference type
private readonly ILogger<UserService> _logger;
// Nullable reference type
private string? _cachedData;
public UserService(ILogger<UserService> logger)
{
_logger = logger;
}
// Return nullable
public string? FindUser(int id)
{
return id > 0 ? $"User{id}" : null;
}
// Parameter nullable
public void UpdateCache(string? data)
{
_cachedData = data;
}
// Null-forgiving operator
public void ProcessData()
{
var data = GetData();
Console.WriteLine(data!.Length); // I know it's not null!
}
private string? GetData() => _cachedData;
}
Async/Await Patterns
public class AsyncPatterns
{
// Async method naming (suffix with Async)
public async Task<string> GetDataAsync()
{
await Task.Delay(100);
return "data";
}
// Avoid async void (except event handlers)
public async void HandleClick(object sender, EventArgs e)
{
await ProcessAsync();
}
// ConfigureAwait(false) in libraries
public async Task<string> LibraryMethodAsync()
{
var data = await GetDataAsync().ConfigureAwait(false);
return data;
}
// Parallel async operations
public async Task<string[]> GetMultipleAsync()
{
var task1 = GetDataAsync();
var task2 = GetDataAsync();
var task3 = GetDataAsync();
return await Task.WhenAll(task1, task2, task3);
}
// Cancellation support
public async Task<string> ProcessWithCancellationAsync(
CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
return "completed";
}
// ValueTask for performance
public async ValueTask<int> GetCachedValueAsync(int key)
{
// Return cached value synchronously if available
if (_cache.TryGetValue(key, out var value))
{
return value;
}
// Otherwise fetch asynchronously
return await FetchValueAsync(key);
}
private readonly Dictionary<int, int> _cache = new();
private async Task<int> FetchValueAsync(int key) => await Task.FromResult(key);
private async Task ProcessAsync() => await Task.CompletedTask;
}
Disposal Patterns
// IDisposable implementation
public class ResourceHandler : IDisposable
{
private bool _disposed;
private readonly Stream _stream;
public ResourceHandler(Stream stream)
{
_stream = stream;
}
public void DoWork()
{
ObjectDisposedException.ThrowIf(_disposed, this);
// Do work
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
// Dispose managed resources
_stream?.Dispose();
}
// Free unmanaged resources
_disposed = true;
}
}
// IAsyncDisposable implementation
public class AsyncResourceHandler : IAsyncDisposable
{
private readonly HttpClient _httpClient;
public AsyncResourceHandler(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async ValueTask DisposeAsync()
{
// Perform async cleanup
_httpClient?.Dispose();
await Task.CompletedTask;
}
}
// Using statement
public class ResourceUser
{
public void UseResource()
{
using var handler = new ResourceHandler(Stream.Null);
handler.DoWork();
} // Dispose called automatically
public async Task UseResourceAsync()
{
await using var handler = new AsyncResourceHandler(new HttpClient());
// Use handler
} // DisposeAsync called automatically
}
Record Types
// Immutable record
public record Person(string FirstName, string LastName, int Age);
// Record with additional members
public record User(int Id, string Email)
{
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
public string FullInfo => $"{Id}: {Email} (Created: {CreatedAt})";
}
// Record inheritance
public record Employee(int Id, string Email, string Department)
: User(Id, Email);
// With expressions (non-destructive mutation)
public class RecordExample
{
public void Example()
{
var person = new Person("John", "Doe", 30);
var olderPerson = person with { Age = 31 };
// Value equality
var person2 = new Person("John", "Doe", 30);
Console.WriteLine(person == person2); // True
}
}
Pattern Matching
public class PatternMatchingExamples
{
public string Describe(object obj)
{
return obj switch
{
null => "null",
int i => $"int: {i}",
string s => $"string: {s}",
Person { Age: >= 18 } => "Adult person",
Person { Age: < 18 } p => $"Minor: {p.FirstName}",
IEnumerable<int> numbers => $"Number sequence: {numbers.Count()}",
_ => "Unknown type"
};
}
public decimal CalculateDiscount(Customer customer)
{
return customer switch
{
{ IsPremium: true, YearsOfMembership: > 5 } => 0.20m,
{ IsPremium: true } => 0.15m,
{ YearsOfMembership: > 3 } => 0.10m,
_ => 0.05m
};
}
}
public record Customer(bool IsPremium, int YearsOfMembership);
Cross-Cutting Patterns
For cross-language comparison and translation patterns, see:
patterns-metaprogramming-dev- Reflection, attributes, source generators, IL emit, dynamic typespatterns-concurrency-dev- Async/await, tasks, parallel programming, thread safetypatterns-serialization-dev- JSON serialization, validation, configuration binding
This skill provides foundational .NET patterns for modern cross-platform development. These patterns cover the .NET runtime, project structure, CLI tools, dependency injection, configuration management, logging, middleware, metaprogramming, NuGet packages, and best practices for building robust .NET applications.