SchematicHQ.Community.DependencyInjection 0.2.0

dotnet add package SchematicHQ.Community.DependencyInjection --version 0.2.0
                    
NuGet\Install-Package SchematicHQ.Community.DependencyInjection -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SchematicHQ.Community.DependencyInjection" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SchematicHQ.Community.DependencyInjection" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="SchematicHQ.Community.DependencyInjection" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SchematicHQ.Community.DependencyInjection --version 0.2.0
                    
#r "nuget: SchematicHQ.Community.DependencyInjection, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SchematicHQ.Community.DependencyInjection@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SchematicHQ.Community.DependencyInjection&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=SchematicHQ.Community.DependencyInjection&version=0.2.0
                    
Install as a Cake Tool

SchematicHQ.Community.AspNetCore

ASP.NET Core integration for Schematic entitlement management.

This repo includes a number of packages that expand the official SchematicHQ.Client SDK with:

  • Automated entitlement checks & tracking for ASP.NET Core routes
  • Integration with Microsoft.Extensions.AI for usage reporting
  • Time-based trait reporting with Quartz.NET
  • DI extensions with ILogger wire-up
  • FusionCache distributed caching support

Packages:

Package Purpose
SchematicHQ.Community.DependencyInjection Registers the Schematic SDK client in DI with ILoggerFactory wiring, plus a FusionCache-backed ICacheProvider and scheduled trait reporting.
SchematicHQ.Community.AspNetCore Feature gating, usage tracking, and identify middleware for ASP.NET Core (net8.0+).
SchematicHQ.Community.Extensions.AI Microsoft.Extensions.AI middleware: meter chat token usage and gate model calls behind entitlements.
SchematicHQ.Community.Extensions.Quartz Quartz.NET integration: gate and track scheduled jobs, and run trait reports on a cron schedule.

Quickstart

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSchematic(builder.Configuration["Schematic:ApiKey"]!);
builder.Services.AddSchematicAspNetCore();
builder.Services.AddSchematicFlagContextResolver<MyFlagContextResolver>();

var app = builder.Build();

app.MapGroup("api").AddSchematicFilters().MapMyEndpoints();
app.MapControllers().AddSchematicFilters();

app.Run();

Tell Schematic who is making the request by implementing a resolver:

public sealed class MyFlagContextResolver : ISchematicFlagContextResolver
{
    public ValueTask<SchematicFlagContext?> ResolveAsync(HttpContext context, CancellationToken ct)
    {
        var companyId = context.User.FindFirstValue("company_id");
        var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
        if (companyId is null || userId is null)
            return ValueTask.FromResult<SchematicFlagContext?>(null); // gate responds 401

        return ValueTask.FromResult<SchematicFlagContext?>(new SchematicFlagContext(
            Company: new() { ["id"] = companyId },
            User: new() { ["id"] = userId }));
    }
}

For simple cases, a delegate works instead of a resolver class: AddSchematicAspNetCore(o => o.ResolveContext = http => ...).

The SDK buffers Track/Identify events and sends them periodically. AddSchematic registers a lifetime hook that calls Schematic.Shutdown() when the host's service provider is disposed, so events buffered at shutdown are flushed instead of lost (bounded at 10 seconds so a broken connection cannot hang shutdown).

Gating endpoints

Minimal APIs:

app.MapGet("/reports", GetReports)
   .RequireFeature("advanced-reports");              // 403 ProblemDetails when not entitled

app.MapPost("/exports", CreateExport)
   .RequireFeature("exports", track: true);          // also tracks an "exports" event on success

Controllers:

[RequireFeature("advanced-reports")]
[HttpGet("reports")]
public IActionResult GetReports() => ...;

A denied check returns RFC 7807 ProblemDetails with status 403, plus featureId and accessDeniedReason extension fields. Customize with options.OnDenied.

Tracking usage

app.MapPost("/messages", SendMessage)
   .TrackFeature("messages-sent", quantity: 1);      // controllers: [TrackFeature("messages-sent")]

Events are emitted only for successful (status < 400) responses, and a tracking failure never fails the response. RequireFeature(..., track: true) reuses the entitlement check result, so the SDK is called once per request.

Identifying customers

builder.Services.AddSchematicIdentifyContextResolver<MyIdentifyResolver>();
...
app.UseSchematicIdentify();

Calls Schematic.Identify for each request whose resolver returns an identity. Set options.IdentifyDeduplicationWindow to send at most one Identify per identity per window.

Receiving webhooks

Verify inbound Schematic webhooks with the signing secret from the dashboard:

builder.Services.AddSchematicAspNetCore(o => o.WebhookSecret = builder.Configuration["Schematic:WebhookSecret"]);
...
app.MapPost("/webhooks/schematic", (JsonElement payload) => Results.Ok())
   .RequireSchematicWebhookSignature();

The filter validates the X-Schematic-Webhook-Signature / X-Schematic-Webhook-Timestamp headers against the raw request body (via the SDK's WebhookVerifier) before the endpoint runs, responding 401 ProblemDetails when they are missing or invalid. The body remains readable by the endpoint afterwards.

Options

builder.Services.AddSchematicAspNetCore(options =>
{
    // How the gate responds when the entitlement check itself fails (network/SDK error).
    // FailClosed (default) => 503 ProblemDetails. FailOpen => request proceeds.
    options.FailurePolicy = SchematicFailurePolicy.FailClosed;

    // Custom denial response.
    options.OnDenied = (http, denial) => Results.Json(new { error = denial.Reason }, statusCode: 402).ExecuteAsync(http);

    // Send at most one Identify per identity in this window (default: every request).
    options.IdentifyDeduplicationWindow = TimeSpan.FromMinutes(5);
});

Caching with FusionCache

The SDK accepts an ICacheProvider for its internal caching. SchematicHQ.Community.DependencyInjection supplies one backed by FusionCache:

builder.Services.AddFusionCache();
builder.Services.AddSchematicFusionCache();          // or AddSchematicFusionCache("cache-name")
builder.Services.AddSchematic(apiKey);               // picks up the registered ICacheProvider

AddSchematic wires any DI-registered ICacheProvider into ClientOptions.CacheProvider unless one was set explicitly, so custom providers plug in the same way. Entries use the SDK's built-in default cache TTL (5 seconds) unless the SDK passes a per-entry TTL; pass AddSchematicFusionCache(defaultTtl: ...) to change it. Note: FusionCache does not support key enumeration, so the provider's DeleteMissing is a no-op — stale entries age out via TTL. The SDK's datastream mode (options.UseDatastream) relies on DeleteMissing to sweep deleted flags during bulk sync, so prefer the SDK's built-in Redis/local cache configuration over this provider when enabling datastream.

Metering AI usage

SchematicHQ.Community.Extensions.AI plugs into the Microsoft.Extensions.AI chat pipeline:

builder.Services.AddHttpContextAccessor();
builder.Services.AddChatClient(sp => /* provider client */)
    .UseSchematicRequireFeature("ai-chat")           // deny before the model is invoked
    .UseSchematicUsageTracking();                    // then meter what allowed calls consume

Tracking reads each response's UsageDetails (streaming included — usage is aggregated across updates and recorded even if the consumer abandons the stream) and emits Track events: by default ai.input-tokens and ai.output-tokens with the model id as a trait, fully remappable via options.MapUsage. Identity comes from the ambient HTTP request's flag-context resolver; set options.FallbackContext for background/non-HTTP calls. Denied gating throws SchematicFeatureDeniedException (with FlagKey/Reason); check failures follow options.FailurePolicy. Tracking failures never fail the AI call.

Anything the provider reports in UsageDetails.AdditionalCounts — cache reads and writes, reasoning tokens — is passed through as ai.{key} with the key normalised to kebab-case, so a Bedrock response carrying cache_read_input_tokens also emits ai.cache-read-input-tokens. These are reported, not interpreted: create a feature only for the counts you want to meter, and check your provider's docs before adding them to the input count, because whether they are already inside InputTokenCount differs by provider (Anthropic reports cache buckets alongside it, OpenAI counts cached tokens within it).

Metering token counts directly works when a feature's price is per token. To meter against credits instead, set the entitlement's priceBehavior to credit_burndown and give each event its own creditConsumptionRate — input and output tokens can burn the same credit at different rates, which keeps the price ratio between them in Schematic rather than hard-coded in a custom MapUsage.

Gating and tracking Quartz jobs

SchematicHQ.Community.Extensions.Quartz applies the same gate/track model to scheduled jobs:

builder.Services.AddSchematicQuartz();                 // options, resolver, listeners
builder.Services.AddQuartz(q =>
{
    q.AddSchematic();                                  // wires the listeners into the scheduler
});

Decorate job classes:

[RequireFeature("nightly-sync")]                       // execution vetoed when not entitled
[TrackFeature("nightly-sync-runs")]                    // tracked after each successful run
public sealed class NightlySyncJob : IJob { ... }

The company/user identity comes from schematic.company.* / schematic.user.* entries in the merged job data map — declare them with .UsingSchematicCompany("id", tenantId) on the job or trigger builder, or register a custom ISchematicJobContextResolver (e.g. reading a tenant accessor). A vetoed execution skips that one firing; the trigger keeps its schedule. Check failures follow AddSchematicQuartz(o => o.FailurePolicy = ...), and tracking failures never fail the job. A job that fans out over many tenants internally cannot be vetoed per-tenant — call ISchematicGateClient inside the loop instead.

Reporting traits on a schedule

Traits hold stateful facts (seat counts, storage used) that entitlements compare against, and are usually computed from your own database. A report is a catalog (which tenants?) plus a source (what are this tenant's traits?):

public sealed class TenantCatalog(CatalogDbContext db) : ISchematicTenantCatalog
{
    public IAsyncEnumerable<string> GetTenantIdsAsync(TraitReportContext context, CancellationToken ct)
        => db.Tenants.Select(t => t.Id).AsAsyncEnumerable();
}

public sealed class SeatSource(ITenantDbContextFactory dbFactory) : ISchematicTraitReportSource
{
    public async Task<CompanyTraitReport?> GetReportAsync(string tenantId, TraitReportContext context, CancellationToken ct)
    {
        await using var db = dbFactory.CreateForTenant(tenantId);
        return new(Keys: new() { ["id"] = tenantId },
                   Traits: new() { ["seats"] = await db.Users.CountAsync(ct) });
    }
}

builder.Services.AddSchematicTraitReport<TenantCatalog, SeatSource>("seats", o => o.Cron = "0 0 3 * * ?");

The source receives each tenant id and handles tenancy itself (a context factory, its own scope — whatever your app uses); return null to skip a tenant. Tenants are processed with bounded parallelism, so acquire per-tenant resources inside the call. With AddSchematicQuartz, every report that sets a cron runs on that schedule (missed runs fire once on startup; trait upserts are last-write-wins, so re-runs are safe). One failing tenant is logged and retried on the next run without sinking the rest. Reports without a cron — or apps not using Quartz — run on demand via ISchematicTraitReportRunner.RunReportAsync("seats").

Set o.ScheduleEnabled = false to keep a report registered and on-demand runnable without anything firing it on a schedule:

builder.Services.AddSchematicTraitReport<TenantCatalog, SeatSource>("seats", o =>
{
    o.Cron = "0 0 3 * * ?";
    o.ScheduleEnabled = !builder.Environment.IsEnvironment("IntegrationTest");
});

An integration test host that boots your real application would otherwise fan out over every tenant and write to Schematic partway through a suite. It also covers nominating one instance to own reporting when several run the same configuration.

Testing your app

The filters call Schematic through the ISchematicGateClient seam. Replace it in tests to run without a live Schematic backend, or register your own implementation to add caching or batching.

For environments with no API key at all — tests, local development, CI, preview deployments — AddSchematicNoOp() registers a client that talks to nothing. AddSchematic rejects a missing key, and the filters, AI middlewares and Quartz listeners all take ISchematicGateClient, so without a stand-in the graph fails at resolve time. Branch on the key so that wiring stays unconditional and those code paths still execute off a key:

var apiKey = builder.Configuration["Schematic:ApiKey"];

if (!string.IsNullOrWhiteSpace(apiKey))
    builder.Services.AddSchematic(apiKey);
else
    builder.Services.AddSchematicNoOp();       // Track/Identify discarded, checks allow

builder.Services.AddSchematicAspNetCore();     // unchanged either way

Track and Identify are discarded, and entitlement checks resolve true so gated features stay reachable — pass AddSchematicNoOp(allowAll: false) to assert denial paths instead. Because it opens every gate, branch on whether a key is configured, not on whether one failed to load: reaching this in production would entitle everybody. Call it instead of AddSchematic, never as well as — whichever registers first wins.

License

Apache-2.0

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on SchematicHQ.Community.DependencyInjection:

Package Downloads
SchematicHQ.Community.AspNetCore

ASP.NET Core integration for Schematic entitlement management: feature gating and usage tracking via endpoint filters, controller attributes, and minimal API conventions.

SchematicHQ.Community.Extensions.Quartz

Quartz.NET integration for Schematic entitlement management: gate and track scheduled jobs via attributes, and schedule recurring trait reports.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 159 8/19/2026
0.1.0 148 8/18/2026