Generate direct saga property accessors instead of extern UnsafeAccessor methods - #7953
Open
danielmarbach wants to merge 8 commits into
Open
danielmarbach wants to merge 8 commits into
danielmarbach wants to merge 8 commits into
Conversation
…sor methods Under the C# 15 / .NET 11 updated memory safety rules every extern member must be marked safe or unsafe (CS9389), which broke generated saga accessors for consumers that opt in. Message and correlation property accessors now read and write the properties directly. A correlation setter that generated code cannot assign (init-only or not accessible from the assembly) keeps an UnsafeAccessor setter on the type that declares it, emitted as safe extern when the compilation uses the updated rules. Properties named like keywords are now escaped.
A nested saga can map a property whose getter is private to the containing type, which the generated file-level accessor cannot call directly. Message and correlation getters now use the same extern fallback as setters, targeting the type that declares the accessor. Also drops a stray approval snapshot and shares the reflection helpers in the accessor execution tests.
danielmarbach
force-pushed
the
saga-accessors-avoid-extern
branch
from
October 1, 2026 16:17
753f665 to
e57d790
Compare
… setter When a correlation property is mapped through an interface that only declares a getter, the setter belongs to the implicit implementation on the saga data type. Its accessibility and init-only checks now apply to that setter, so a private or init-only implementation keeps the extern accessor instead of failing to compile.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The saga generator emits
[UnsafeAccessor] static externmethods for every message property getter and every correlation property getter and setter. That is what we ship in 10.2. With the C# 15 / .NET 11 updated memory safety rules, everyexternmember has to be markedsafeorunsafe, otherwise the compiler reports CS9389. I verified this on the .NET 11 RC1 SDK withLangVersion=previewand theupdated-memory-safety-rulesfeature flag: the generated code from 10.2 doesn't compile. The opt-in is preview today, and I'd rather not wait for it to become the default before we fix the generated code.The accessors live in the consumer's assembly, so most properties can be reached with plain C#. This PR emits direct access (
message.Prop,((TSagaData)sagaData).Prop, and an assignment for the setter) and keepsUnsafeAccessoronly for accessors that generated code can't call:private set,protected set, or aprivate geton a property that a nested saga maps)For those, the extern targets the type that declares the accessor. It is emitted as
static safe externwhen the compilation uses the updated rules and as plainstatic externotherwise. The detection and thesafemarker follow whatSystem.Text.Jsondoes:UsesUpdatedMemorySafetyRulesreadsIModuleSymbol.MemorySafetyRulesVersionthrough reflection and falls back to theupdated-memory-safety-rulesfeature flag, and the emitter addssafeto its externs when that returns true. It also uses direct access wherever it can and only falls back toUnsafeAccessorotherwise, which is the same split I'm making here. The flag is only evaluated for the sagas that need an extern accessor, so everyone else keeps their cached generator output when it changes.Observable effects:
externmembers for the common case, and it compiles with and without the new rules.MissingMethodExceptionon the firstWriteTo, because the accessor targeted the derived type. The extern now targets the type that declares the accessor.UnsafeAccessorand wouldn't compile with direct access, so those getters keep the extern.m => ((IHasId)m).Id) is read through that interface. Mapped through an explicit implementation, it used to throwMissingMethodExceptionat runtime, because the old accessor looked forget_Idon the concrete type. The same applies to correlation properties on saga data. If the interface only declares a getter and the saga data class implements aprivate setor init-only setter, the extern targets that setter.@event) are escaped. The old code passed names as strings, so it worked there. Direct access needs the@.[Obsolete]property is covered by the CS0612/CS0618 suppression the generated files already have. Nullable warnings are already disabled in generated files, so the new casts can't surface nullability warnings.Alternatives I considered:
ExpressionBasedCorrelationPropertyAccessorfor init-only setters. That would have worked and is simpler, but those sagas would silently lose the generated, reflection free accessor they get in 10.2. I think that's the wrong thing to change in a minor, even if the fallback still works under AOT.UnsafeAccessoreverywhere and only addingsafe. This is the smallest change. I'm not convinced it's worth it, because every accessor then depends on the detection and on a keyword that may still change before it ships.What I couldn't verify:
MemorySafetyRulesVersionisn't on the publicIModuleSymbol, so only the feature flag path is active today. The reflection path is guarded by a return type check so a future shape change can't throw, but nothing exercises it yet.safe externat compile time on RC1 only. I didn't run the resulting program there. The runtime round trips are covered by the analyzer tests on .NET 10.KeyedServiceCollectionAdapterin Core and a test local accessor inSagaMetadataCreationTestsstill useUnsafeAccessorand would hit CS9389 if the NServiceBus projects themselves opted in. I left those alone since they don't affect consumers.