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.
// 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.
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.
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.
{
"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.