-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix support for externally declared records #9104
Conversation
There's a bit of code duplication in the PR, but I tried to keep it in line with that was already there (i.e. the field accessor generation is also duplicated for the original use case too), and I didn't find a generic "SymbolExtensions" to centralize the primary constructor lookup, for example. Let me know if I should seek to unify that and the existing code a bit too. |
f85d424
to
d2fb2f0
Compare
Failed test seems unrelated:
It runs fine on my machine 🤔 Perhaps use dotnet-retest 😉 |
8527186
to
58a972b
Compare
Any updates on this @ReubenBond? This is blocking a release of my cloud actors thingy :) |
I'll review this asap and see when we can get a release out |
58a972b
to
60aedf2
Compare
I pushed a failing (#ifdef'd out) test for the generic case, which I believe is fixed in 9.0: dotnet/runtime#99468 |
.Where(m => m.MethodKind == MethodKind.Constructor && m.Parameters.Length > 0) | ||
// Check for a ctor where all parameters have a corresponding compiler-generated prop. | ||
.FirstOrDefault(ctor => ctor.Parameters.All(prm => | ||
properties.Any(prop => prop.Name.Equals(prm.Name, StringComparison.Ordinal) && prop.IsCompilerGenerated()))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How reliable is this? Could some language/compiler change in the future break it and cause a wire compatibility issue for users?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't find anything more reliable than that :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As sharplab shows, there's nothing else we can depend on, unless the runtime adds something: https://sharplab.io/#v2:EYLgtghglgdgNAExAagD4AEDMACATgUwGMB7XBbAMWOIAp0BGABmwCEJcBKAbiA=. The property itself isn't [CompilerGenerated]
, and I doubt anyone would manually write an accessor annotated with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, I think the proposed heuristics is as specific and detailed as possible to detect the situation given the current compiler output.
FIxes improper codec codegen for records declared in referenced projects/assemblies. Roslyn does not guarantee the symbols contain the backing fields for generated properties (see dotnet/roslyn#72374 (comment)) and it also doesn't even report `record struct` symbols as records at all (see dotnet/roslyn#69326). This makes for a very inconsistent experience when dealing with types defined in external assemblies that don't use the Orleans SDK themselves. We implement a heuristics here to determine primary constructors that can be relied upon to detect them consistently: 1. A ctor with non-zero parameters 2. All parameters match by name exactly with corresponding properties 3. All matching properties have getter AND setter annotated with [CompilerGenerated]. In addition, since the backing field isn't available at all in these records, and the corresponding property isn't settable (it's generated as `init set`), we leverage unsafe accessors (see https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.unsafeaccessorattribute?view=net-8.0) instead. The code checks whether the `FieldAccessorDescription` has an initializer syntax or not to determine whether to generate the original code or the new accessor version. The signature of the accessor matches the delegate that is generated for the regular backing field case, so there is no need to modify other call sites. Fixes #9092
In externally defined symbols, we need to also check for `[CompilerGenerated]`.
This allows running F5 on the generator project and troubleshoot issues. By checking for an already attached debugger, we can debug also scenarios of code generation where the roslyn component starts it with DTB=true.
a4976d6
to
28ffda8
Compare
@ReubenBond do you foresee this PR being an issue to merge? |
.OfType<IMethodSymbol>() | ||
.Where(m => m.MethodKind == MethodKind.Constructor && m.Parameters.Length > 0) | ||
// Check for a ctor where all parameters have a corresponding compiler-generated prop. | ||
.FirstOrDefault(ctor => ctor.Parameters.All(prm => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this variation easier to read, and possibly a little faster.
.FirstOrDefault(ctor =>
ctor.Parameters
.Join(properties,
prm => prm.Name,
prop => prop.Name,
(prm, prop) => prop)
.Count(prop => prop.IsCompilerGenerated) == ctor.Parameters.Length
);
@codymullins no, I just need to run more tests to give myself confidence that this will not accidentally break backwards compatibility. I'll try to get to that today. |
To add to the argument for merging this PR: the externally defined records that are now considered for generation would have previously been entirely omitted, causing runtime errors. So I think the risk is virtually non existent? |
I hit this today, it occurred to me that GenerateSerializer already validates the type if it's applicable (if I'm not mistaken, that means either Id is present or it's a record). Why is there a need to validate externally declared types as well? Furthermore, my impression was that GenerateSerializer will actually do the generation as well in the current assembly and then a client will use those? |
If the type is externally defined only, and the client is generic and does not do codegen itself, you would face this issue, since the serializers won't exist in the assembly where the records are defined. |
Since it already references orleans bits for GenerateSerializer attribute, wouldn't it make sense to also do the codegen right there? That is, include the generator inside Abstractions package. See https://github.com/dotnet/extensions/blob/main/src/Libraries/Microsoft.Extensions.Telemetry.Abstractions/Microsoft.Extensions.Telemetry.Abstractions.csproj for another instance of this pattern. If that's too much of a change I'd argue at least the validation should happen via an analyzer once the attribute is applied so you don't need to validate externally - the presence of the attribute is effectively a guarantee. (This is also generally how language features expose their contract for example for NRT, the compiler validates on declaration only, then reads the medatada on the client side, there's no concept of external validation because you wouldn't have enough info to do it after the fact which I think is the core issue here.) |
That's a different issue. The If that was not an explicit goal/supported scenario, then there would be no reason for the attribute to be separate from the Orleans SDK/generator itself, since using one would require using the other. |
28ffda8
to
f0ca3a4
Compare
f0ca3a4
to
050c1c9
Compare
Apologies for the hiatus, my newborn arrived early, so I've had my hands full. I pushed a couple of additional changes:
I'm ready to merge when CI completes. |
Congrats!! 👶 rocks ❤️ |
Many thanks, @kzu! |
Would be awesome to know the milestone this will ship with, if you happen to know, to track when I can start using it 🙏 |
Fixes improper codec codegen for records declared in referenced projects/assemblies. Roslyn does not guarantee the symbols contain the backing fields for generated properties (see dotnet/roslyn#72374 (comment)) and it also doesn't even report
record struct
symbols as records at all (see dotnet/roslyn#69326).This makes for a very inconsistent experience when dealing with types defined in external assemblies that don't use the Orleans SDK themselves.
We implement a heuristics here to determine primary constructors that can be relied upon to detect them consistently:
In addition, since the backing field isn't available at all in these records, and the corresponding property isn't settable (it's generated as
init set
), we leverage unsafe accessors (see https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.unsafeaccessorattribute?view=net-8.0) instead. The code checks whether theFieldAccessorDescription
has an initializer syntax or not to determine whether to generate the original code or the new accessor version.The signature of the accessor matches the delegate that is generated for the regular backing field case, so there is no need to modify other call sites.
Fixes #9092
Microsoft Reviewers: Open in CodeFlow