Skip to content

Conversation

@stakx
Copy link
Member

@stakx stakx commented Dec 7, 2025

This is the smallest code change I can think of off the top of my head to enable PersistentAssemblyBuilder.SaveAssembly() on .NET 9+. It represents only a code exploration to test feasibility, but it is far from an ideal solution and won't be merged in this form.

The approach chosen here works using the following approach:

  • Dynamic types are generated using .NET 9's new PersistedAssemblyBuilder. The major obstacle here is that it does not support type activation. In order to activate a type, the assembly is emitted into a stream, which can then be loaded into the current assembly load context like any regular assembly.

  • This means that the dynamic assembly is "baked" after every single proxy type activation, and it cannot be reused to emit further types. Because of that, the ModuleScope gets replaced with a new one after every single proxy type creation.

  • Because of the ModuleScope recycling, we essentially lose the type cache functionality. In theory, this functionality could be restored by lifting the type cache out of ModuleScope into DefaultProxyBuilder (so that it can span several module scopes). This refactoring would be fairly involved if breaking changes in the public API were to be avoided.

Something similar to this might be good enough to run out test suite (including PE / IL verification of generated assemblies) on .NET 9, as we could likely do well without the type cache in this scenario.

Comment on lines +105 to +107
#if !NET9_0_OR_GREATER
InitializeStaticFields(proxyType);
#endif
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must be skipped here because the generated type cannot be activated while tied to a PersitedAssemblyBuilder.

Comment on lines +135 to +143
lastAssemblyGenerated?.Dispose();
var stream = new MemoryStream();
persistedAssemblyBuilder.Save(stream);
stream.Seek(0, SeekOrigin.Begin);
var assembly = AssemblyLoadContext.Default.LoadFromStream(stream);
type = assembly.GetType(type.FullName!)!;
lastAssemblyGenerated = stream;
lastScope = scope;
scope = scope.Recycle();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would likely have to be made thread-safe.

using System.Collections.Generic;
using System.Linq;
using System.Reflection;
#if NET9_0_OR_GREATER
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using NET9_0_OR_GREATER we'd probably want a FEATURE_PERSISTEDASSEMBLYBUILDER conditional compilation symbol.

Comment on lines +468 to +470
#if NET9_0_OR_GREATER
assemblyFilePath = weakModulePath;
#else
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This alternative assignment may actually work also for .NET 4.6.2+, as [Weak|Strong]NamedModule.FullyQualifiedName is possibly equal to [weak|strong]ModulePath.

Comment on lines +500 to +502
#elif NET9_0_OR_GREATER
var persistedAssemblyBuilder = (PersistedAssemblyBuilder)assemblyBuilder;
persistedAssemblyBuilder.Save(assemblyFilePath);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is left-over code that (if it worked) would have to be called by the PersistentProxyBuilder on its lastScope. This code has been replaced with code directly in PersistentProxyBuilder however... probably not ideal.

Comment on lines +51 to +57
var assemblyPath = lastScope.WeakNamedModule != null ? lastScope.WeakNamedModuleName : lastScope.StrongNamedModuleName;
using var file = File.Create(assemblyPath);
lastAssemblyGenerated.Seek(0, SeekOrigin.Begin);
lastAssemblyGenerated.CopyTo(file);
file.Flush();
file.Close();
return assemblyPath;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should perhaps be replaced with return lastScope.SaveAssembly();.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant