Build-Time Entitlement Gates with Roslyn
Краткое содержание
Pixel’s announcement explores building a Roslyn source generator for Keyright entitlements. Developers declare gates with attributes, while generated code applies consistent license checks and validates entitlement names at compile time, helping prevent typos and inconsistent access control.
DEV Community
Gate features at compile time: a Roslyn source generator for Keyright entitlements
Entitlement checks have a way of multiplying. One feature needs export.pdf, another needs reports.scheduled, a third needs a seats limit — and each one grows its own little block of "resolve the license, read the entitlement, decide what to do on failure." Copy that block around a codebase and it drifts: one call site throws, another returns false, a third forgets to check at all, and somewhere a string literal says exprot.pdf and silently denies a feature to every paying customer. The checks are individually trivial and collectively a liability. This is exactly the kind of repetitive, error-prone, mechanical code a Roslyn source generator exists to remove. Instead of writing the guard, you declare the gate with an attribute and let the generator emit a correct, uniform guard at compile time — and, because the generator sees your whole compilation, it can validate every entitlement name against your declared catalog and fail the build on a typo. This post builds that generator against the Keyright SDK. What we are building You decorate a partial method with the entitlement it requires: public static partial class Features
{
[Gate("export.pdf")]
public static partial void RequirePdfExport();
[Gate("export.pdf", Kind = GateKind.Limit, Limit = "pages-per-day")]
public static partial int PdfPageBudget();
}
The generator fills in the bodies. At a call site you write one line and the branching is gone: public byte[] Export(Document doc)
{
Features.RequirePdfExport(); // throws LicenseException if not entitled
int budget = Features.PdfPageBudget(); // the numeric limit from the signed license
...
}
Every gate now reads the same way, fails closed the same way, and names an entitlement the compiler has already confirmed exists. The diagram below shows the two halves — what happens at build time, and what the emitted code does at runtime. A source generator emits entitlement guards at compile time; the guard enforces the signed license at runtime Compile time Runtime [Gate("export.pdf")] partial method entitlements.json declared catalog Source generator Emitted guard Features.g.cs unknown name → build error KR0001 guard call site LicenseGate.Current cached LicenseInfo Validate() signature + public key allow throw compiles in The marker attribute Source generators ship their own attributes so consumers do not reference a separate assembly. Emit it during post-initialization, before any user code is analyzed: private const string AttributeSource = """
namespace Acme.Licensing;
[System.AttributeUsage(System.AttributeTargets.Method)]
internal sealed class GateAttribute : System.Attribute
{
public GateAttribute(string entitlement) => Entitlement = entitlement;
public string Entitlement { get; }
public GateKind Kind { get; set; } = GateKind.Require;
public string? Limit { get; set; }
}
internal enum GateKind { Require, Limit }
""";
Require emits a void guard that throws when the entitlement is absent; Limit emits a method returning the numeric limit the signed license carries for a named key. The incremental generator Use IIncrementalGenerator — the cached, pull-based model that only re-runs when relevant inputs change. Three inputs feed it: the attribute (registered up front), the annotated methods, and the entitlement manifest supplied as an AdditionalFile. [Generator]
public sealed class EntitlementGateGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(ctx =>
ctx.AddSource("GateAttribute.g.cs", AttributeSource));
IncrementalValuesProvider<GateModel> gates = context.SyntaxProvider
.ForAttributeWithMetadataName(
"Acme.Licensing.GateAttribute",
predicate: static (node, _) => node is MethodDeclarationSyntax m
&& m.Modifiers.Any(SyntaxKind.Pa...

Powered by Readybot.io