dotnet-sdk icon indicating copy to clipboard operation
dotnet-sdk copied to clipboard

Add source generator for strongly-typed OpenFGA model and relationships in the .NET SDK

Open simongottschlag opened this issue 1 month ago • 5 comments

Checklist

Describe the problem you'd like to have solved

When using the .NET SDK, developers must currently work with raw strings for object types, relationships and tuples when constructing ClientCheckRequest and related payloads. This leads to:

  • Lack of compile-time safety (typos or invalid relations are only caught at runtime)
  • Poor discoverability and DX (developers must remember the model definition manually)
  • Fragile integration when the OpenFGA model changes

For example, building a check request today requires writing things like "channel:abc", "reader", or "team:xyz" manually, even though this information exists in the OpenFGA model.

Describe the ideal solution

It would be ideal if the SDK provided an optional source generator (or similar tooling) that:

  • Parses an OpenFGA model file (DSL or JSON) at build time
  • Generates strongly-typed wrappers for each type in the model (User, Team, Channel, etc.)
  • Generates enum values or constants for valid relationships per type (Reader, ReadWriter, etc.)
  • Optionally generates typed helpers for tuples and ClientCheckRequest creation

Example of generated types:

var user = User.From("oid123");
var team = Team.From("team-1");
var channel = Channel.From("chan-42");

var check = ChannelChecks.Build(
    user,
    ChannelRelationship.Reader,
    channel,
    ChannelTuples.Parent(team, channel)
);

This gives developers full IntelliSense support and compile-time safety while maintaining parity with the underlying OpenFGA model.

Alternatives and current workarounds

I currently work around this by writing a custom source generator that:

  • Deserializes a model.transformed.json file embedded at build time
  • Emits classes and enums for all types and relationships
  • Generates small helper builders for tuples and check requests

While this approach works, it adds maintenance burden and fragments the ecosystem. Having this functionality built into the official SDK would ensure consistency and encourage type-safe integrations across .NET applications.

References

No response

Additional context

  • Using .NET 9 and Roslyn incremental source generators
  • Model schema example:
model
  schema 1.1

type user

type team
  relations
    define reader: [user] or read_writer
    define read_writer: [user]

type channel
  relations
    define parent: [team]
    define reader: [user] or reader from parent
    define read_writer: [user] or read_writer from parent
  • The generated code makes ClientCheckRequest construction strongly typed and much easier to maintain:
var req = ChannelChecks.Build(
    User.From(oid),
    ChannelRelationship.Reader,
    Channel.From(channelId),
    ChannelTuples.Parent(Team.From(teamId), Channel.From(channelId))
);

simongottschlag avatar Oct 18 '25 06:10 simongottschlag

If you're interested, I have an example here but it's not an SG:

https://github.com/openfga/dotnet-sdk/issues/137

It just generates text source instead since this does not need SG IMO

CharlieDigital avatar Oct 22 '25 16:10 CharlieDigital

If you're interested, I have an example here but it's not an SG:

https://github.com/openfga/dotnet-sdk/issues/137

It just generates text source instead since this does not need SG IMO

Hi @CharlieDigital!

I think the link you pasted went to this issue instead of somewhere with the example 😊

simongottschlag avatar Oct 22 '25 20:10 simongottschlag

Too many tabs open.

https://github.com/CharlieDigital/dn-openfga

You can see usage here: https://github.com/CharlieDigital/dn-openfga/blob/main/tests/FluentPermissionsTest.Crm.cs

        await permissions
            .ToMutate()
            // Add the group `us_east_team_401` and add `alice_401` as a member
            .Add<User, Group>("alice_401", g => g.Member, "us_east_team_401")
            .Add<User, Group>("bob_401", g => g.Member, "us_east_team_401")
            // Add the group `us_east_team_401` as an reader on the company `acme_corp_401`
            .Add<Group, CrmCompany>("us_east_team_401", c => c.Reader, "acme_corp_401")
            // Add `alice_401` directly as an owner on the company `acme_corp_401`
            .Add<User, CrmCompany>("alice_401", c => c.Owner, "acme_corp_401")
            // The `CrmPerson` `potential_customer_401` has the parent `CrmCompany` `acme_corp_401`
            // So it should inherit the block from parent company.
            .Assign<CrmCompany, CrmPerson>("acme_corp_401", c => c.Parent, "potential_customer_401")
            .SaveChangesAsync(TestContext.Current.CancellationToken);

Source "building" (not using source gen): https://github.com/CharlieDigital/dn-openfga/blob/main/targets/Program.cs

Process is .fga -> .json -> .cs artifacts

Generates stubs for conditions as well:

https://github.com/CharlieDigital/dn-openfga/blob/main/tests/FluentPermissionsTest.Tiers.cs#L24-L52

        var permissions = Permissions.WithClient(client);

        await permissions
            .ToMutate()
            .Add<Team, Subscription>(
                "acme_corp_2001",
                s => s.FreeTrial,
                "sub_5678",
                c => c.ForActiveTrial(TimeSpan.FromDays(10), DateTime.Parse("2026-01-01T00:00:00Z"))
            )
            .SaveChangesAsync(TestContext.Current.CancellationToken);

        // Now check that we can access within the trial period
        var canAccessWithin10Days = await permissions
            .ToValidate()
            .Has<Team, Subscription>(
                "acme_corp_2001",
                s => s.FreeTrial,
                "sub_5678",
                c => c.ActiveTrialContext(DateTime.Parse("2026-01-05T00:00:00Z"))
            )
            .ValidateSingleAsync(TestContext.Current.CancellationToken);

Sample generated file: https://github.com/CharlieDigital/dn-openfga/blob/main/tests/Models/PermissionScopes.g.cs

CharlieDigital avatar Oct 22 '25 22:10 CharlieDigital

@CharlieDigital really cool! Thank you for sharing!

simongottschlag avatar Oct 23 '25 19:10 simongottschlag

@simongottschlag Thanks for submitting this feature request. We've added this to our queue for discussion.

dyeam0 avatar Oct 30 '25 14:10 dyeam0