Structuring a real application
Put it all together: a layered solution (Domain, Application, Infrastructure, Presentation), the dependency rule, MVVM for WPF, configuration, logging and tests, plus where to go after this course.
The dependency rule
Solution layout
SteelTools.sln├─ src/│ ├─ SteelTools.Domain/ Beam, Connection, LoadCase, rules (no references)│ ├─ SteelTools.Application/ use cases, IBeamRepository, IReportWriter (→ Domain)│ ├─ SteelTools.Infrastructure/ EF Core, IFC reader, PDF, email (→ Application)│ └─ SteelTools.Wpf/ views, view models, App.xaml.cs (→ Application, Infrastructure)└─ tests/ ├─ SteelTools.Domain.Tests/ └─ SteelTools.Application.Tests/Domain: the engineering core
namespace SteelTools.Domain; public sealed class Beam{ public string Mark { get; } public double LengthM { get; } public double KgPerM { get; } public Beam(string mark, double lengthM, double kgPerM) { if (string.IsNullOrWhiteSpace(mark)) throw new ArgumentException("Mark is required", nameof(mark)); if (lengthM <= 0) throw new ArgumentOutOfRangeException(nameof(lengthM)); (Mark, LengthM, KgPerM) = (mark, lengthM, kgPerM); } public double WeightKg() => Math.Round(LengthM * KgPerM, 1); public bool IsOverUtilised(double utilisation) => utilisation > 1.0;}using of databases, JSON or WPF.
Application: use cases
namespace SteelTools.Application; public interface IBeamRepository{ Task<IReadOnlyList<Beam>> GetByLevelAsync(int level, CancellationToken ct);} public record BeamResult(string Mark, double WeightKg, bool Ok); public class CheckBeamsHandler(IBeamRepository repo, ILogger<CheckBeamsHandler> log){ public async Task<IReadOnlyList<BeamResult>> HandleAsync(int level, CancellationToken ct) { var beams = await repo.GetByLevelAsync(level, ct); log.LogInformation("Checking {Count} beams on level {Level}", beams.Count, level); return beams.Select(b => new BeamResult(b.Mark, b.WeightKg(), b.WeightKg() < 2000)).ToList(); }}Infrastructure: the details
namespace SteelTools.Infrastructure; public class SqlBeamRepository(SteelDbContext db) : IBeamRepository{ public async Task<IReadOnlyList<Beam>> GetByLevelAsync(int level, CancellationToken ct) => await db.BeamRows.Where(b => b.Level == level) // BeamRow: the table's shape, not the domain class .Select(b => new Beam(b.Mark, b.LengthM, b.KgPerM)) .ToListAsync(ct);} public static class DependencyInjection{ public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config) { services.AddDbContext<SteelDbContext>(o => o.UseSqlServer(config.GetConnectionString("Steel"))); services.AddScoped<IBeamRepository, SqlBeamRepository>(); services.AddTransient<IReportWriter, PdfReportWriter>(); return services; }}Presentation: WPF with MVVM
public partial class BeamListViewModel(CheckBeamsHandler handler) : ObservableObject{ public ObservableCollection<BeamResult> Results { get; } = new(); [ObservableProperty] private int _level = 1; [ObservableProperty] private bool _isBusy; [RelayCommand] private async Task CheckAsync(CancellationToken ct) { IsBusy = true; try { Results.Clear(); foreach (var r in await handler.HandleAsync(Level, ct)) Results.Add(r); } finally { IsBusy = false; } }}public partial class App : Application{ private IHost _host = null!; protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var builder = Host.CreateApplicationBuilder(); builder.Services.AddInfrastructure(builder.Configuration); // appsettings.json builder.Services.AddTransient<CheckBeamsHandler>(); builder.Services.AddTransient<BeamListViewModel>(); builder.Services.AddTransient<MainWindow>(); _host = builder.Build(); _host.Services.GetRequiredService<MainWindow>().Show(); } protected override void OnExit(ExitEventArgs e) { _host.Dispose(); base.OnExit(e); }} // MainWindow.xaml.cs: the view model arrives through the constructorpublic partial class MainWindow : Window{ public MainWindow(BeamListViewModel vm) { InitializeComponent(); DataContext = vm; }}Cross-cutting essentials
| Concern | Use | Notes |
|---|---|---|
appsettings.json + IConfiguration / IOptions<T> | ||
ILogger<T> (+ Serilog for files) | "{Count} beams", not string concatenation | |
async all the way from the view model to the database | CancellationToken through every layer |
Keep it proportional
You finished the C# course
Console.WriteLine("Hello") to layered, tested, dependency-injected applications. Practise by building a real tool from start to finish. Some ideas, roughly in order of difficulty:
A console BOM tool: read a CSV of members, group by profile with LINQ, write totals to JSON. A WPF beam checker with MVVM, async loading and unit tests for the checks. A Revit or Tekla add-in that reads model elements through the vendor API and reuses your Domain project. An ASP.NET Core web API serving the same Application layer to a browser front-end.
CheckBeamsHandler with a fake IBeamRepository that returns two beams: B1 (6 m × 42.2 kg/m) and B2 (30 m × 122 kg/m). Assert that B1 is OK and B2 is not. Use NullLogger<CheckBeamsHandler>.Instance for the logger.
Show a hint
Show the solution
class FakeRepo(params Beam[] beams) : IBeamRepository{ public Task<IReadOnlyList<Beam>> GetByLevelAsync(int level, CancellationToken ct) => Task.FromResult<IReadOnlyList<Beam>>(beams);} public class CheckBeamsHandlerTests{ [Fact] public async Task Heavy_beam_is_flagged() { var repo = new FakeRepo(new Beam("B1", 6, 42.2), new Beam("B2", 30, 122)); var handler = new CheckBeamsHandler(repo, NullLogger<CheckBeamsHandler>.Instance); var results = await handler.HandleAsync(level: 1, CancellationToken.None); Assert.True(results.Single(r => r.Mark == "B1").Ok); Assert.False(results.Single(r => r.Mark == "B2").Ok); }}Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1
Remember
Layers: Domain (rules) ← Application (use cases, interfaces) ← Infrastructure and Presentation (details). Dependencies point inwards; interfaces live with the code that needs them. One composition root wires everything with DI; MVVM keeps WPF views thin and testable. Scale the structure to the app; keep the dependency rule from day one.