The trade offs are though that patterns and behind the scenes source code generation is another layer that the devs who have to follow need to deal with when debugging and understanding why something isn’t working. They either spend more time understanding the bespoke things or are bottle necked relying on a team or person to help them get through those moments. It’s a trade off and one that has bit me and others before

I am not talking about C# specifically but also and I agree.

Implicit and magic looks nice at first but sometimes it can be annoying. I remember the first time I tried Ruby On Rails and I was looking for a piece of config.

Yes, "convention over configuration". Namely, ungreppsble and magic.

This kind of stuff must be used with a lot of care.

I usually favor explicit and, for config, plain data (usually toml).

This can be extended to hidden or non-obvious allocations and other stuff (when I work with C++).

It is better to know what is going on when you need to and burying it in a couole of layers can make things unnecessarily difficult.

Would you rather a team move faster and be more productive or be a purist and disallow abstractions to avoid some potential runtime tracing challenges which can be mitigated with good use of OTEL and logging? I don't know about you, but I'm going to bias towards productivity and use integration tests + observability to safeguard code.

Disallow bespoke abstractions and use the industry standard ones instead. People who make abstractions inflate how productive they’re making everyone else. Your user base is much smaller than popular libs, so your docs and abstractions are not as battle tested and easy to use as much as you think.

This is raw OpenFGA code:

    await client.Write(
        new ClientWriteRequest(
            [
                // Alice is an admin of form 123
                new()
                {
                    Object = "form:124",
                    Relation = "editor",
                    User = "user:avery",
                },
            ]
        )
    );

    var checkResponse = await client.Check(
        new ClientCheckRequest
        {
            Object = "form:124",
            Relation = "editor",
            User = "user:avery",
        }
    );

    var checkResponse2 = await client.Check(
        new ClientCheckRequest
        {
            Object = "form:125",
            Relation = "editor",
            User = "user:avery",
        }
    );
This is an abstraction we wrote on top of it:

    await Permissions
        .WithClient(client)
        .ToMutate()
        .Add<User, Form>("alice", "editor", "226")
        .Add<User, Team>("alice", "member", "motion")
        .SaveChangesAsync();

    var allAllowed = await Permissions
        .WithClient(client)
        .ToValidate()
        .Can<User, Form>("alice", "edit", "226")
        .Has<User, Team>("alice", "member", "motion")
        .ValidateAllAsync();
You would make the case that the former is better than the latter?

In the first example, I have to learn and understand OpenFGA, in the second example I have to learn and understand OpenFGA and your abstractions.

Well the point of using abstractions is that you don't need to know the things that it is abstracting. I think the abstraction here is self explaining what it does and you can certainly understand and use it without needing to understand all the specifics behind it.

More importantly: it prevents "usr:alice_123" instead of "user:alice_123" by using the type constraint to generate the prefix for the identifier.

How much faster are we talking? Because you'd have to account for the time lost debugging annotations.

What are you working on that you're debugging annotations everyday? I'd say you've made a big mistake if you're doing that/you didn't read the docs and don't understand how to use the attribute.

(Of course you are also free to write C# without any of the built in frameworks and write purely explicit handling and routing)

On the other hand, we write CRUD every day so anything that saves repetition with CRUD is a gain.

I don't debug them every day, but when I do, it takes days for a nasty bug to be worked out.

Yes, they make CRUD stuff very easy and convenient.

It has been worth the abstraction in my organization with many teams. Thinking 1000+ engineers, at minimum. It helps to abstract as necessary for new teammates that want to simply add a new endpoint yet follow all the legal, security, and data enforcement rules.

Better than no magic abstractions imo. In our large monorepo, LSP feedback can often be so slow that I can’t even rely on it to be productive. I just intuit and pattern match, and these magical abstractions do help. If I get stuck, then I’ll wade into the docs and code myself, and then ask the owning team if I need more help.

That's the deal with all metaprogramming.