dotnet-sdk
dotnet-sdk copied to clipboard
Add source generator for strongly-typed OpenFGA model and relationships in the .NET SDK
Checklist
- [x] I agree to the terms within the OpenFGA Code of Conduct.
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))
);
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
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 😊
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 really cool! Thank you for sharing!
@simongottschlag Thanks for submitting this feature request. We've added this to our queue for discussion.