Feature Detection
The unified generator doesn’t have a configuration file. Instead it asks the Roslyn compilation: “is this marker type reachable?”. If yes, the corresponding feature is activated for this build. If no, the generator stays silent — no stale code, no useless files.
This is the composition-by-presence principle in action: add a NuGet, the generator notices; remove it, the generated code disappears.
Mechanism
Section titled “Mechanism”// Pragmatic.SourceGenerator/Core/FeatureDetector.cs (verbatim shape)internal static class FeatureDetector{ public static DetectedFeatures Detect(Compilation compilation) => new() { HasActions = TypeExists(compilation, "Pragmatic.Actions.Attributes.DomainActionAttribute"), HasEndpoints = TypeExists(compilation, "Pragmatic.Endpoints.Attributes.EndpointAttribute"), HasPersistenceEFCore = TypeExists(compilation, "Pragmatic.Persistence.EFCore.PragmaticDbContextAttribute"), HasMessaging = TypeExists(compilation, "Pragmatic.Messaging.Attributes.MessageHandlerAttribute"), HasJobs = TypeExists(compilation, "Pragmatic.Jobs.Attributes.RecurringJobAttribute"), HasResult = TypeExists(compilation, "Pragmatic.Result.IError"), // … ~32 flags total + EfCoreProvider };
private static bool TypeExists(Compilation compilation, string fullyQualifiedName) => compilation.GetTypeByMetadataName(fullyQualifiedName) is not null;}Detect runs once per compilation and returns an immutable DetectedFeatures record. Each flag is read by the generator pipelines to decide whether to run — e.g. JobsFeature (which is actually registered unconditionally) and MessagingFeature only emit when their marker type resolves.
The flags
Section titled “The flags”A representative subset of the ~32 flags. The complete, authoritative list — including the exact marker FQNs — lives in DetectedFeatures.cs and FeatureDetector.cs. FQNs below are copied verbatim from FeatureDetector.cs.
| Flag | Marker type (FQN) | Module |
|---|---|---|
HasActions | Pragmatic.Actions.Attributes.DomainActionAttribute | Pragmatic.Actions |
HasValidation | Pragmatic.Validation.Attributes.ValidationAttribute | Pragmatic.Validation |
HasCaching | Pragmatic.Caching.Attributes.CacheableAttribute | Pragmatic.Caching |
HasMapping | Pragmatic.Mapping.Attributes.MapFromAttribute`1 | Pragmatic.Mapping |
HasEndpoints | Pragmatic.Endpoints.Attributes.EndpointAttribute | Pragmatic.Endpoints |
HasPersistence | Pragmatic.Persistence.Query.Attributes.QueryAttribute`2 | Pragmatic.Persistence |
HasPersistenceEFCore | Pragmatic.Persistence.EFCore.PragmaticDbContextAttribute | Pragmatic.Persistence.EFCore |
HasPatch | Pragmatic.Patch.Attributes.GeneratePatchAttribute`1 | Pragmatic.Patch |
HasComposition | (detected by CompositionDetector) | Pragmatic.Composition(.Host) |
HasI18n | Pragmatic.Internationalization.Attributes.TranslationKeysAttribute | Pragmatic.Internationalization |
HasResult | Pragmatic.Result.IError | Pragmatic.Result |
HasConfiguration | Pragmatic.Configuration.ConfigurationAttribute | Pragmatic.Configuration |
HasResilience | Pragmatic.Resilience.Attributes.ResiliencePolicyAttribute | Pragmatic.Resilience |
HasIdentityAspNetCore | Pragmatic.Identity.Authorization.PragmaticPermissionRequirement | Pragmatic.Identity (ASP.NET Core) |
HasIdentityPersistence | Pragmatic.Identity.Persistence.Entities.IdentityUserBase`1 | Pragmatic.Identity.Persistence |
HasAuthorization | Pragmatic.Authorization.PragmaticBuilderAuthorizationExtensions | Pragmatic.Authorization |
HasMultiTenancy | Pragmatic.MultiTenancy.ITenantContext | Pragmatic.MultiTenancy |
HasFeatureFlags | Pragmatic.FeatureFlags.FeatureFlagServiceCollectionExtensions | Pragmatic.FeatureFlags |
HasDiscovery | Pragmatic.Discovery.Abstractions.IDiscoveryService | Pragmatic.Discovery |
HasTemporal | Pragmatic.Temporal.Clock.SystemClock | Pragmatic.Temporal |
HasEventsEFCore | Pragmatic.Events.EFCore.DomainEventsInterceptor | Pragmatic.Events.EFCore |
HasMessaging | Pragmatic.Messaging.Attributes.MessageHandlerAttribute | Pragmatic.Messaging |
HasMessagingEFCore | Pragmatic.Messaging.EFCore.Outbox.OutboxInterceptor | Pragmatic.Messaging.EFCore |
HasMessagingChannels | Pragmatic.Messaging.Channels.ChannelTransport | Pragmatic.Messaging.Channels |
HasMessagingRabbitMq | Pragmatic.Messaging.RabbitMQ.RabbitMqTransport | Pragmatic.Messaging.RabbitMQ |
HasMessagingAuditing | Pragmatic.Messaging.Auditing.IAuditStore | Pragmatic.Messaging.Auditing |
HasMessagingSagas | Pragmatic.Messaging.Saga.ISagaRepository`1 | Pragmatic.Messaging (sagas) |
HasMessagingJobs | Pragmatic.Messaging.Jobs.PublishMessageJob | Pragmatic.Messaging.Jobs |
HasJobs | Pragmatic.Jobs.Attributes.RecurringJobAttribute | Pragmatic.Jobs |
HasMigrations | Pragmatic.Migrations.Schema.SchemaVersion | Pragmatic.Migrations |
HasComments | Pragmatic.Comments.HasCommentsAttribute | Pragmatic.Comments (Traits) |
HasControlPlane | Pragmatic.ControlPlane.IControlPlane | Pragmatic.ControlPlane |
HasNotifications | Pragmatic.Notifications.INotificationService | Pragmatic.Notifications |
IsHostMode / IsHostCompositionMode | (entry-point + Composition.Host ref) | host detection |
EF Core provider detection
Section titled “EF Core provider detection”Beyond the boolean flags, FeatureDetector resolves an EfCoreProvider enum so the persistence templates can emit provider-specific SQL. It probes the referenced EF Core provider assemblies in priority order:
| Probe (marker type) | Result |
|---|---|
Microsoft.EntityFrameworkCore.NpgsqlDbContextOptionsBuilderExtensions | PostgreSql |
Microsoft.EntityFrameworkCore.SqlServerDbContextOptionsBuilderExtensions | SqlServer |
Microsoft.EntityFrameworkCore.SqliteDbContextOptionsBuilderExtensions | Sqlite |
| (none of the above) | Generic |
PostgreSQL wins over SQL Server, which wins over SQLite — so a project referencing more than one provider resolves to the highest-priority match.
Why a marker type instead of assembly name?
Section titled “Why a marker type instead of assembly name?”Three reasons, in order of importance:
- Meta-packages don’t break detection. If a
Pragmatic.Compositionmeta-package pulls inPragmatic.Composition.HostplusPragmatic.Actions, detection by assembly name would produce duplicate triggers. Detection by type fires exactly once per real type, independent of how you got there. - Renames are decoupled. If a module is ever renamed at the assembly level, the type-level marker can stay stable — detection keeps working until the real API moves.
- Fast at compile time.
GetTypeByMetadataNameis one of the cheapest Roslyn lookups. No reflection walk, no assembly enumeration.
Cross-feature composition
Section titled “Cross-feature composition”Detection is not binary-only: when multiple features are active, they can compose. Example:
HasActionsandHasPersistenceEFCore→ the Action template can emitLoadEntityinvocations that go through the generated repository.HasMessagingandHasPersistenceEFCore→ the Messaging template emits an outbox that uses theDbContextfrom the persistence feature.HasAuthorizationandHasActions→ the Action invoker gets a permission check filter automatically.
These cross-feature enrichers live in the top-level Compositions/ folder of the generator and run after the per-feature transforms.
Adding a detection probe
Section titled “Adding a detection probe”When you add a new feature:
public bool HasMyFeature { get; init; }// Core/FeatureDetector.cs — inside the Detect(...) object initializerHasMyFeature = TypeExists(compilation, "Pragmatic.MyFeature.SomeMarkerType"),The rest of the generator pipeline can then guard its work on detected.HasMyFeature. Tests cover the matrix of “feature absent” vs “feature present” to ensure the generator produces zero output when the module is not referenced.
Debugging detection
Section titled “Debugging detection”If a feature is unexpectedly silent:
- Open
obj/Generated/and check whether any_Infra.*.Registration.g.cswas emitted for that feature. If missing, the flag was false. - Verify the project file references the module NuGet (not just a transitive via meta-package — meta-packages work too, but check the graph with
dotnet list package --include-transitive). - Ensure the marker type name matches the current
FeatureDetector.cs— types get renamed occasionally across minor versions.