Windows Forms App

WinForms has no built-in DI container the way ASP.NET Core does, but Microsoft.Extensions.Hosting can host one anyway — meaning a desktop app can reuse the exact same Application/Infrastructure layers and Shared DTOs as the API and every other frontend (see Layered Architecture & DI), instead of reimplementing data access.

Bootstrapping DI in Program.cs

The generic host gives WinForms the same ServiceCollection/IServiceProvider machinery ASP.NET Core uses, just without a web server attached.

Program.cs
// dotnet add package Microsoft.Extensions.Hosting

internal static class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();

        var host = Host.CreateDefaultBuilder()
            .ConfigureServices((context, services) =>
            {
                services.RegisterApplication();
                services.RegisterInfrastructure(
                    context.Configuration.GetConnectionString("DefaultConnection")!);

                // every form the app can open is itself a resolvable service
                services.AddTransient<MainForm>();
                services.AddTransient<ResourceEditForm>();
            })
            .Build();

        var mainForm = host.Services.GetRequiredService<MainForm>();
        Application.Run(mainForm);
    }
}

Forms take their dependencies through the constructor

Because MainForm is resolved from the container rather than new'd up, constructor injection works exactly like it does in a controller or a Blazor component.

MainForm.cs
public partial class MainForm : Form
{
    private readonly IResourceService _resourceService;
    private readonly IServiceProvider _serviceProvider;

    public MainForm(IResourceService resourceService, IServiceProvider serviceProvider)
    {
        InitializeComponent();
        _resourceService = resourceService;
        _serviceProvider = serviceProvider;
    }

    private async void MainForm_Load(object sender, EventArgs e)
    {
        var resources = await _resourceService.GetAllAsync();
        resourceGrid.DataSource = resources.ToList();
    }

    private void EditButton_Click(object sender, EventArgs e)
    {
        // resolve a child form through the container too, so it gets its own dependencies
        using var editForm = _serviceProvider.GetRequiredService<ResourceEditForm>();
        if (editForm.ShowDialog(this) == DialogResult.OK)
            MainForm_Load(this, EventArgs.Empty);
    }
}

Long-running work: never block the UI thread

WinForms has one UI thread. An async event handler (like MainForm_Load above) keeps the form responsive during I/O, but touching a control from a background thread throws — marshal back via Invoke/BeginInvoke, or stick to async/await which already returns to the UI thread's SynchronizationContext automatically.

reporting progress from a background operation
private async void ImportButton_Click(object sender, EventArgs e)
{
    var progress = new Progress<int>(percent => progressBar.Value = percent);

    importButton.Enabled = false;
    try
    {
        await _importService.ImportAsync(filePath, progress); // reports via IProgress
    }
    finally
    {
        importButton.Enabled = true;
    }
}

Config the same way as the API

Same Strongly-Typed Configuration approach — an appsettings.json next to the executable, bound to an options class, instead of scattering connection strings across app.config or hardcoding them.

appsettings.json (Copy to Output Directory)
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=YourApp;Trusted_Connection=True;TrustServerCertificate=True"
  }
}
This is the same trick as Building a CLI Tool — a non-web front end hosting the generic DI container so it can reuse the backend's Application/Infrastructure layers verbatim. Between the API, Blazor, Angular, a CLI tool, and WinForms, the actual business logic exists in exactly one place.