<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. C#
  3. Master level
All lessons in this course

Lesson 60 of 60 19 minMaster

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.

A 200-line console app can live in one file. A real tool used by a team for years cannot. This final lesson combines everything in the course (classes, interfaces, DI, async, tests, SOLID) into a structure that stays understandable as it grows.

The dependency rule

Arrange the code in layers like an onion. Dependencies point inwards only. The centre holds your engineering knowledge and depends on nothing; the outer layers hold replaceable details: database, files, UI.

Interactive

Where does this code go?

Pick a piece of code, then click the layer you think it belongs to.

Can this project reference that one?
→
✗ Application → Infrastructure: dependencies must point inwards. If Application needs something from Infrastructure, define an interface in Application and let Infrastructure implement it.

Score: 0 / 10

Solution layout

Folder structure
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/

Separate projects (not just folders) let the compiler enforce the rule: if Domain doesn’t reference Infrastructure, nobody can accidentally use EF Core inside a beam.

Domain: the engineering core

Domain/Beam.cs
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;}

Entities protect their own rules: you can’t create a beam with no mark or a negative length. No using of databases, JSON or WPF.

Application: use cases

Application/CheckBeams.cs
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();    }}

The handler orchestrates: get data through an interface, apply domain rules, return simple results. It is easy to unit test with a fake repository.

Infrastructure: the details

Infrastructure/SqlBeamRepository.cs
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

MVVM (Model-View-ViewModel) keeps the window (View, XAML) thin. All state and actions live in a ViewModel that the View binds to, and it can be tested without opening a window.

Wpf/BeamListViewModel.cs
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; }    }}
Wpf/App.xaml.cs (composition root)
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

ConcernUseNotes
Configurationappsettings.json + IConfiguration / IOptions<T>no hard-coded paths or connection strings; secrets in user-secrets or environment variables
LoggingILogger<T> (+ Serilog for files)structured messages: "{Count} beams", not string concatenation
Errorsvalidate at the edges, exceptions for the unexpectedone global handler that logs and shows a friendly message
Testsunit tests for Domain/Application, a few integration tests for Infrastructurethe inner layers are the easiest and most valuable to test
Asyncasync all the way from the view model to the databasepass CancellationToken through every layer

Keep it proportional

You finished the C# course

You went from 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:

  1. A console BOM tool: read a CSV of members, group by profile with LINQ, write totals to JSON.
  2. A WPF beam checker with MVVM, async loading and unit tests for the checks.
  3. A Revit or Tekla add-in that reads model elements through the vendor API and reuses your Domain project.
  4. An ASP.NET Core web API serving the same Application layer to a browser front-end.

Next tracks on this site: Data structures and algorithms, BIM math and SQL. They build directly on what you learned here.

Quick check

Which reference breaks the dependency rule?

Quick check

CheckBeamsHandler needs to load beams from SQL Server. Where should the interface IBeamRepository be declared?

Practice

Write a unit test for 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

30 × 122 = 3660 kg, which is above the 2000 kg limit in the handler.

Show the solution
Solution.cs
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);    }}
Output
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.
0 of 60 lessons completed