Skip to content

Getting Started with Pragmatic.Composition

This guide walks through setting up a Pragmatic application from scratch, adding modules, and understanding how the pieces connect.

  • .NET 10 SDK
  • Pragmatic.Abstractions (attributes and interfaces)
  • Pragmatic.Composition.Host (ASP.NET Core hosting runtime)
  • Pragmatic.SourceGenerator (analyzer reference)

The simplest Pragmatic application needs three things:

  1. A module class declaring the deployment topology
  2. A Program.cs calling PragmaticApp.RunAsync
  3. The source generator wired as an analyzer
Terminal window
dotnet new web -n MyApp.Host
cd MyApp.Host
dotnet add reference ../Pragmatic.Composition/src/Pragmatic.Composition/Pragmatic.Composition.csproj

Add the source generator in your .csproj:

<ProjectReference Include="..\Pragmatic.SourceGenerator\src\Pragmatic.SourceGenerator\Pragmatic.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />

Create an empty sealed class decorated with [Module]:

using Pragmatic.Composition.Attributes;
namespace MyApp.Host;
[Module]
public sealed class MyAppModule;

This is the simplest possible module — no database, no dependencies. The SG detects it as the host module because the project is an executable.

using Pragmatic.Composition.Hosting;
await PragmaticApp.RunAsync(args).ConfigureAwait(false);

That is the entire Program.cs. The source generator creates the real PragmaticApp.RunAsync implementation based on discovered modules. The stub in Pragmatic.Composition.Host exists only for design-time IntelliSense before the generator runs.

Domain logic lives in separate class library projects. Each module declares itself with [Module] and its dependencies with [IncludeModule<T>].

Terminal window
dotnet new classlib -n MyApp.Catalog
MyApp.Catalog/CatalogModule.cs
using Pragmatic.Composition.Attributes;
namespace MyApp.Catalog;
[Module(Name = "MyApp.Catalog", Description = "Product catalog management")]
public sealed class CatalogModule;

Reference the module project from the host and add it to the topology:

MyApp.Host/MyAppModule.cs
using Pragmatic.Composition.Attributes;
using MyApp.Catalog;
namespace MyApp.Host;
[Module]
[Include<CatalogModule>]
public sealed class MyAppModule;

If the module needs persistence, declare a database and wire it:

MyApp.Host/MyAppDatabase.cs
using Pragmatic.Composition.Attributes;
using Pragmatic.Composition.Database;
using Pragmatic.Composition.Enums;
namespace MyApp.Host;
[PragmaticDatabase(Provider = DatabaseProvider.SqlServer, ConfigKey = "ConnectionStrings:App")]
public sealed class MyAppDatabase : PragmaticDatabase { }

Then update the module topology:

[Module]
[Include<CatalogModule, MyAppDatabase>]
public sealed class MyAppModule;

The SG generates a DbContext for the Catalog boundary, wired to the App database connection string.

When a module depends on types from another module (e.g., Booking needs Catalog for property lookups), declare it with [IncludeModule<T>]:

MyApp.Booking/BookingModule.cs
[Module(Name = "MyApp.Booking", Description = "Reservation management")]
[IncludeModule<CatalogModule>]
public sealed class BookingModule;

The host then includes both:

[Module]
[Include<CatalogModule, MyAppDatabase>]
[Include<BookingModule, MyAppDatabase>]
public sealed class MyAppModule;

Infrastructure choices are configured in Program.cs via the IPragmaticBuilder callback:

await PragmaticApp.RunAsync(args, app =>
{
// Development convenience
if (app.Environment.IsDevelopment())
app.UseDatabaseEnsureCreated();
// Multi-tenancy
app.UseMultiTenancy(mt => mt.UseHeader());
// Authentication
app.UseAuthentication<NoOpAuthenticationHandler>("PragmaticDefault");
// Logging
app.UseLogging(log =>
{
log.AddConsole(PragmaticConsoleConfiguration.ForDevelopment());
});
}).ConfigureAwait(false);

Each Use*() method comes from the corresponding Pragmatic module’s extension methods on IPragmaticBuilder. You only see methods for packages you have referenced.

Application-specific services and middleware go in an IStartupStep implementation:

using Pragmatic.Composition.Abstractions;
using Pragmatic.Composition.Attributes;
namespace MyApp.Host;
[StartupStep]
[RequiresConfig("ConnectionStrings:App")]
public class MyAppStartupStep : IStartupStep
{
public int Order => 60;
public void ConfigureServices(
IServiceCollection services,
IConfiguration configuration,
IHostEnvironment environment)
{
// Register app-specific services
services.AddOpenApi();
}
public void ConfigurePipeline(IApplicationBuilder app)
{
app.UseAuthentication();
app.UseAuthorization();
}
}

The [RequiresConfig] attribute makes the host validate that ConnectionStrings:App exists at startup. If missing, the application fails fast with a clear error message.

Mark services with [Service] in your domain modules. The SG generates all DI registration code:

MyApp.Catalog/Services/ProductService.cs
using Pragmatic.Composition.Attributes;
namespace MyApp.Catalog.Services;
[Service]
public class ProductService(IProductRepository repository) : IProductService
{
// Implementation
}

No manual services.AddScoped<IProductService, ProductService>() needed. The SG generates it and emits a [PragmaticMetadata] assembly attribute. The host generator reads it and includes the registration in the aggregated startup code.

A typical Pragmatic application looks like this:

MyApp.Host/
Program.cs # PragmaticApp.RunAsync(args, app => { ... })
MyAppModule.cs # [Module] + [Include<...>] topology
MyAppDatabase.cs # PragmaticDatabase declaration
MyAppStartupStep.cs # [StartupStep] + IStartupStep
MyApp.Catalog/
CatalogModule.cs # [Module] declaration
Services/
ProductService.cs # [Service] auto-registered
Entities/
Product.cs # Domain entity
MyApp.Booking/
BookingModule.cs # [Module] + [IncludeModule<CatalogModule>]
Services/
ReservationService.cs # [Service]