| name | dotnet-expert |
| description | .NET development principles and decision-making. .NET 8, ASP.NET Core, C# 12, dependency injection, best practices. |
.NET Expert - Modern .NET 8
.NET development principles and decision-making.
Learn to THINK, not copy-paste patterns.
⚠️ How to Use This Skill
This skill teaches decision-making principles for .NET development.
- ASK user for project type (web API, worker, blazor)
- Choose minimal APIs vs controller-based
- Don't use outdated .NET Framework patterns
1. Framework Selection
Decision Tree
What are you building?
│
├── REST API
│ ├── Minimal APIs (NEW .NET 6+) - Recommended for simple APIs
│ └── Controller-based - Complex operations, heavy validation
│
├── Full-stack Web
│ ├── Blazor Server - Server-rendered, WebSocket
│ ├── Blazor WebAssembly - Client-side, PWA-ish
│ └── ASP.NET Core MVC - Traditional Razor
│
├── Background Worker
│ └── .NET Worker Services + Quartz/Hangfire
│
├── gRPC Services
│ └── .NET gRPC (protobuf)
│
├── Desktop
│ ├── WPF (.NET 8)
│ ├── WinForms (legacy)
│ └── MAUI (cross-platform)
│
└── Microservices
└── .NET 8 + Dapr (cloud-native patterns)
Comparison: Minimal APIs vs Controllers
| Factor | Minimal APIs | Controllers |
|---|
| Boilerplate | Very Low | Medium |
| Routing | Inline/Fluent | Attribute-based |
| Validation | Manual/FluentValidation | DataAnnotations |
| Best For | Microservices, simple APIs | Enterprise, complex validation |
| OpenAPI/Swagger | Built-in | Route-based |
2. Modern C# 12 Features
Primary Constructors
public class UserService(
IUserRepository userRepository,
IEmailService emailService)
{
public User? GetById(int id)
{
return userRepository.FindById(id);
}
}
public record User(int Id, string Name, string Email);
Collection Expressions
var list = new List<int> { 1, 2, 3 };
int[] array = new int[] { 1, 2, 3 };
var list = [1, 2, 3];
int[] array = [1, 2, 3];
var dict = new Dictionary<string, int> { {"a", 1} };
var dict2 = ["a", 1 ];
LINQ Improvements
var result = items
.Where(x => x.IsActive)
.OrderBy(x => x.Name)
.Select(x => x.ToDto())
.ToList();
var countByCategory = products.CountBy(p => p.Category);
Pattern Matching
public string Describe(object obj) => obj switch
{
int i => $"Integer: {i}",
string s => $"String: {s.ToUpper()}",
User { Name: "Admin" } => "Admin user!",
User u => $"User: {u.Name}",
null => "Null!",
_ => $"Type: {obj.GetType().Name}"
};
3. Dependency Injection
.NET 8 DI (Built-in)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddTransient<IEmailService, EmailService>();
builder.Services.AddSingleton<IConfigService, ConfigService>();
builder.Services.Configure<DatabaseOptions>(
builder.Configuration.GetSection("Database"));
var app = builder.Build();
app.MapGet("/users/{id}", (IUserRepository repo, int id)
=> repo.FindById(id));
Constructor Injection
public class UserService(IUserRepository repository)
{
private readonly IUserRepository _repo = repository;
public User? GetById(int id) => _repo.FindById(id);
}
public record EmailService(SmtpConfig Config) : IEmailService
{
public async Task SendAsync(string to, string subject, string body)
{
}
}
4. Minimal APIs (Recommended)
Basic Pattern
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddScoped<IUserRepository, UserRepository>();
var app = builder.Build();
app.MapGet("/api/users", async (AppDbContext db)
=> await db.Users.ToListAsync());
app.MapGet("/api/users/{id}", async (int id, AppDbContext db)
=> await db.Users.FindAsync(id));
app.MapPost("/api/users", async (User user, AppDbContext db) =>
{
db.Users.Add(user);
await db.SaveChangesAsync();
return Results.Created($"/api/users/{user.Id}", user);
});
app.Run();
With Validation
public record CreateUserRequest(string Name, string Email);
app.MapPost("/api/users", async (CreateUserRequest request,
IValidator<CreateUserRequest> validator,
AppDbContext db) =>
{
var validation = await validator.ValidateAsync(request);
if (!validation.IsValid)
return Results.BadRequest(validation.Errors);
var user = new User { Name = request.Name, Email = request.Email };
db.Users.Add(user);
await db.SaveChangesAsync();
return Results.Created($"/api/users/{user.Id}", user);
});
5. Database Access
Entity Framework Core
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) {}
public DbSet<User> Users => Set<User>();
public DbSet<Order> Orders => Set<Order>();
}
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Email { get; set; } = "";
public DateTime CreatedAt { get; set; }
}
var users = await context.Users
.Where(u => u.IsActive)
.OrderBy(u => u.Name)
.Select(u => new UserDto(u.Id, u.Name))
.ToListAsync();
Dapper for Performance
public class UserRepository : IUserRepository
{
private readonly DbConnection _connection;
public async Task<User?> FindById(int id)
{
return await _connection.QueryFirstOrDefaultAsync<User>(
"SELECT * FROM Users WHERE Id = @Id",
new { Id = id });
}
}
Comparison
| Approach | Best For | Performance |
|---|
| EF Core | Most CRUD, migrations | Good |
| Dapper | Raw SQL, high performance | Excellent |
| ADO.NET | Minimal overhead | Best |
6. Testing Patterns
xUnit with Moq
public class UserServiceTest
{
[Fact]
public void GetById_WhenExists_ReturnsUser()
{
var mockRepo = new Mock<IUserRepository>();
mockRepo.Setup(r => r.FindById(1))
.Returns(new User(1, "test@test.com"));
var service = new UserService(mockRepo.Object);
var result = service.GetById(1);
Assert.NotNull(result);
Assert.Equal("test@test.com", result.Email);
}
}
public class UserApiTest : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public UserApiTest(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetUsers_ReturnsOk()
{
var response = await _client.GetAsync("/api/users");
Assert.True(response.IsSuccessStatusCode);
}
}
Testing Tools
| Tool | Purpose |
|---|
| xUnit | Unit/Integration testing |
| Moq | Mocking framework |
| FluentAssertions | Fluent assertions |
| Testcontainers.Sqlite | In-memory DB testing |
| WireMock | HTTP mocking |
7. Configuration
Options Pattern
public class DatabaseOptions
{
public string ConnectionString { get; set; } = "";
public int MaxConnections { get; set; } = 10;
}
{
"Database": {
"ConnectionString": "Server=.;Database=mydb;",
"MaxConnections": 20
}
}
builder.Services.Configure<DatabaseOptions>(
builder.Configuration.GetSection("Database"));
public class UserService(
IOptions<DatabaseOptions> options)
{
public string ConnString =>
options.Value.ConnectionString;
}
8. Error Handling
Global Exception Handler
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(
"{\"error\": \"An unexpected error occurred\"}");
});
});
app.MapPost("/api/users", async ([FromBody] User user) =>
{
try
{
}
catch (Exception ex)
{
return Results.Problem(detail: ex.Message,
statusCode: 400);
}
});
9. Decision Checklist
Before implementing:
❌ Anti-Patterns to Avoid
❌ DON'T:
- Use synchronous I/O in API endpoints
- Configure EF Core without tracking changes awareness
- Use ViewBag/ViewData in minimal APIs
- Skip health checks in microservices
- Use .NET Framework patterns in .NET 8
- Skip connection string encryption for production
✅ DO:
- Use Primary Constructors (C# 12)
- Use records for DTOs and responses
- Use Minimal APIs for simple services
- Use IAsyncEnumerable for streaming
- Configure health checks (/healthz)
- Use FluentValidation for complex validation
Remember: .NET 8 is modern and productive.
Use Minimal APIs, records, and primary constructors.
Think in modern .NET, not .NET Framework.