Skip to content

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.

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

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.

FlagMarker type (FQN)Module
HasActionsPragmatic.Actions.Attributes.DomainActionAttributePragmatic.Actions
HasValidationPragmatic.Validation.Attributes.ValidationAttributePragmatic.Validation
HasCachingPragmatic.Caching.Attributes.CacheableAttributePragmatic.Caching
HasMappingPragmatic.Mapping.Attributes.MapFromAttribute`1Pragmatic.Mapping
HasEndpointsPragmatic.Endpoints.Attributes.EndpointAttributePragmatic.Endpoints
HasPersistencePragmatic.Persistence.Query.Attributes.QueryAttribute`2Pragmatic.Persistence
HasPersistenceEFCorePragmatic.Persistence.EFCore.PragmaticDbContextAttributePragmatic.Persistence.EFCore
HasPatchPragmatic.Patch.Attributes.GeneratePatchAttribute`1Pragmatic.Patch
HasComposition(detected by CompositionDetector)Pragmatic.Composition(.Host)
HasI18nPragmatic.Internationalization.Attributes.TranslationKeysAttributePragmatic.Internationalization
HasResultPragmatic.Result.IErrorPragmatic.Result
HasConfigurationPragmatic.Configuration.ConfigurationAttributePragmatic.Configuration
HasResiliencePragmatic.Resilience.Attributes.ResiliencePolicyAttributePragmatic.Resilience
HasIdentityAspNetCorePragmatic.Identity.Authorization.PragmaticPermissionRequirementPragmatic.Identity (ASP.NET Core)
HasIdentityPersistencePragmatic.Identity.Persistence.Entities.IdentityUserBase`1Pragmatic.Identity.Persistence
HasAuthorizationPragmatic.Authorization.PragmaticBuilderAuthorizationExtensionsPragmatic.Authorization
HasMultiTenancyPragmatic.MultiTenancy.ITenantContextPragmatic.MultiTenancy
HasFeatureFlagsPragmatic.FeatureFlags.FeatureFlagServiceCollectionExtensionsPragmatic.FeatureFlags
HasDiscoveryPragmatic.Discovery.Abstractions.IDiscoveryServicePragmatic.Discovery
HasTemporalPragmatic.Temporal.Clock.SystemClockPragmatic.Temporal
HasEventsEFCorePragmatic.Events.EFCore.DomainEventsInterceptorPragmatic.Events.EFCore
HasMessagingPragmatic.Messaging.Attributes.MessageHandlerAttributePragmatic.Messaging
HasMessagingEFCorePragmatic.Messaging.EFCore.Outbox.OutboxInterceptorPragmatic.Messaging.EFCore
HasMessagingChannelsPragmatic.Messaging.Channels.ChannelTransportPragmatic.Messaging.Channels
HasMessagingRabbitMqPragmatic.Messaging.RabbitMQ.RabbitMqTransportPragmatic.Messaging.RabbitMQ
HasMessagingAuditingPragmatic.Messaging.Auditing.IAuditStorePragmatic.Messaging.Auditing
HasMessagingSagasPragmatic.Messaging.Saga.ISagaRepository`1Pragmatic.Messaging (sagas)
HasMessagingJobsPragmatic.Messaging.Jobs.PublishMessageJobPragmatic.Messaging.Jobs
HasJobsPragmatic.Jobs.Attributes.RecurringJobAttributePragmatic.Jobs
HasMigrationsPragmatic.Migrations.Schema.SchemaVersionPragmatic.Migrations
HasCommentsPragmatic.Comments.HasCommentsAttributePragmatic.Comments (Traits)
HasControlPlanePragmatic.ControlPlane.IControlPlanePragmatic.ControlPlane
HasNotificationsPragmatic.Notifications.INotificationServicePragmatic.Notifications
IsHostMode / IsHostCompositionMode(entry-point + Composition.Host ref)host 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.NpgsqlDbContextOptionsBuilderExtensionsPostgreSql
Microsoft.EntityFrameworkCore.SqlServerDbContextOptionsBuilderExtensionsSqlServer
Microsoft.EntityFrameworkCore.SqliteDbContextOptionsBuilderExtensionsSqlite
(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:

  1. Meta-packages don’t break detection. If a Pragmatic.Composition meta-package pulls in Pragmatic.Composition.Host plus Pragmatic.Actions, detection by assembly name would produce duplicate triggers. Detection by type fires exactly once per real type, independent of how you got there.
  2. 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.
  3. Fast at compile time. GetTypeByMetadataName is one of the cheapest Roslyn lookups. No reflection walk, no assembly enumeration.

Detection is not binary-only: when multiple features are active, they can compose. Example:

  • HasActions and HasPersistenceEFCore → the Action template can emit LoadEntity invocations that go through the generated repository.
  • HasMessaging and HasPersistenceEFCore → the Messaging template emits an outbox that uses the DbContext from the persistence feature.
  • HasAuthorization and HasActions → 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.

When you add a new feature:

Core/DetectedFeatures.cs
public bool HasMyFeature { get; init; }
// Core/FeatureDetector.cs — inside the Detect(...) object initializer
HasMyFeature = 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.

If a feature is unexpectedly silent:

  1. Open obj/Generated/ and check whether any _Infra.*.Registration.g.cs was emitted for that feature. If missing, the flag was false.
  2. 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).
  3. Ensure the marker type name matches the current FeatureDetector.cs — types get renamed occasionally across minor versions.