diff --git a/CHANGELOG.md b/CHANGELOG.md index 2be2b6c03..e0f1a5922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Breaking Changes: Enhancements: - Minimally improved support for methods having `ref struct` parameter and return types, such as `Span`: Intercepting such methods caused the runtime to throw `InvalidProgramException` and `NullReferenceException` due to forbidden conversions of `ref struct` values when transferring them into & out of `IInvocation` instances. To prevent these exceptions from being thrown, such values now get replaced with `null` in `IInvocation`, and with `default` values in return values and `out` arguments. When proceeding to a target, the target methods likewise receive such nullified values. (@stakx, #665) +- Restore ability on .NET 9 and later to save dynamic assemblies to disk using `PersistentProxyBuilder` (@stakx, #718) - Dependencies were updated Bugfixes: diff --git a/ref/Castle.Core-net9.0.cs b/ref/Castle.Core-net9.0.cs index 7b416367c..ff07f7f7d 100644 --- a/ref/Castle.Core-net9.0.cs +++ b/ref/Castle.Core-net9.0.cs @@ -2585,6 +2585,16 @@ public ModuleScope(bool savePhysicalAssembly, bool disableSignedModule, string s public string WeakNamedModuleName { get; } public static byte[] GetKeyPair() { } } + public sealed class PersistentProxyBuilder : Castle.DynamicProxy.DefaultProxyBuilder + { + public PersistentProxyBuilder() { } + public event System.EventHandler? AssemblyCreated; + } + public sealed class PersistentProxyBuilderAssemblyEventArgs : System.EventArgs + { + public System.Reflection.Assembly Assembly { get; } + public byte[] AssemblyBytes { get; } + } public class ProxyGenerationOptions { public static readonly Castle.DynamicProxy.ProxyGenerationOptions Default; diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/PersistentProxyBuilderTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/PersistentProxyBuilderTestCase.cs index 64e95bf6e..67b216a39 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/PersistentProxyBuilderTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/PersistentProxyBuilderTestCase.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2021 Castle Project - http://www.castleproject.org/ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,14 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -#if FEATURE_ASSEMBLYBUILDER_SAVE - namespace Castle.DynamicProxy.Tests { using System; + using System.Collections.Generic; using System.IO; + using System.Linq; + using System.Reflection; + + using Castle.DynamicProxy.Tests.Interfaces; + using NUnit.Framework; +#if NET462_OR_GREATER + [TestFixture] public class PersistentProxyBuilderTestCase { @@ -44,6 +50,55 @@ public void PersistentProxyBuilder_SavesSignedFile() Assert.IsTrue(path.EndsWith(ModuleScope.DEFAULT_FILE_NAME)); } } -} -#endif \ No newline at end of file +#elif NET9_0_OR_GREATER + + [TestFixture] + [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] + public class PersistentProxyBuilderTestCase + { + private List assemblies; + private PersistentProxyBuilder builder; + + [SetUp] + public void SetupProxyBuilder() + { + assemblies = new List(); + builder = new PersistentProxyBuilder(); + builder.AssemblyCreated += (object _, PersistentProxyBuilderAssemblyEventArgs e) => + { + assemblies.Add(e.Assembly); + }; + } + + [Test] + public void SavesOneAssemblyPerProxiedType() + { + var oneProxyType = builder.CreateInterfaceProxyTypeWithoutTarget(typeof(IOne), Type.EmptyTypes, ProxyGenerationOptions.Default); + Assert.AreEqual(1, assemblies.Count); + + var twoProxyType = builder.CreateInterfaceProxyTypeWithoutTarget(typeof(ITwo), Type.EmptyTypes, ProxyGenerationOptions.Default); + Assert.AreEqual(2, assemblies.Count); + + var oneAssembly = assemblies[0]; + var twoAssembly = assemblies[1]; + Assert.AreSame(oneAssembly, oneProxyType.Assembly); + Assert.AreSame(twoAssembly, twoProxyType.Assembly); + Assert.AreNotSame(oneAssembly, twoAssembly); + } + + [Test] + public void TypeCacheWorks() + { + var proxyType1 = builder.CreateClassProxyType(typeof(object), Type.EmptyTypes, ProxyGenerationOptions.Default); + var proxyType2 = builder.CreateClassProxyType(typeof(object), Type.EmptyTypes, ProxyGenerationOptions.Default); + + Assert.AreEqual(1, assemblies.Count); + Assert.AreSame(proxyType1, proxyType2); + Assert.AreSame(proxyType1.Assembly, proxyType2.Assembly); + } + } + +#endif + +} diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/ClassEmitter.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/ClassEmitter.cs index bc87365dc..df24467ff 100644 --- a/src/Castle.Core/DynamicProxy/Generators/Emitters/ClassEmitter.cs +++ b/src/Castle.Core/DynamicProxy/Generators/Emitters/ClassEmitter.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2025 Castle Project - http://www.castleproject.org/ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -412,7 +412,7 @@ public Type BuildType() builder.Generate(); } - var type = typeBuilder.CreateTypeInfo(); + var type = moduleScope.BuildType(typeBuilder); return type; } diff --git a/src/Castle.Core/DynamicProxy/ModuleScope.cs b/src/Castle.Core/DynamicProxy/ModuleScope.cs index 578360e9b..908b91a44 100644 --- a/src/Castle.Core/DynamicProxy/ModuleScope.cs +++ b/src/Castle.Core/DynamicProxy/ModuleScope.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2021 Castle Project - http://www.castleproject.org/ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -311,50 +311,59 @@ internal ModuleBuilder ObtainDynamicModuleWithWeakName() private ModuleBuilder CreateModule(bool signStrongName) { - var assemblyName = GetAssemblyName(signStrongName); + var assemblyBuilder = CreateAssembly(signStrongName); var moduleName = signStrongName ? StrongNamedModuleName : WeakNamedModuleName; -#if FEATURE_APPDOMAIN +#if NET462_OR_GREATER if (savePhysicalAssembly) { - AssemblyBuilder assemblyBuilder; - try - { - assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( - assemblyName, AssemblyBuilderAccess.RunAndSave, signStrongName ? StrongNamedModuleDirectory : WeakNamedModuleDirectory); - } - catch (ArgumentException e) - { - if (signStrongName == false && e.StackTrace.Contains("ComputePublicKey") == false) - { - // I have no idea what that could be - throw; - } - var message = string.Format( - "There was an error creating dynamic assembly for your proxies - you don't have permissions " + - "required to sign the assembly. To workaround it you can enforce generating non-signed assembly " + - "only when creating {0}. Alternatively ensure that your account has all the required permissions.", - GetType()); - throw new ArgumentException(message, e); - } - var module = assemblyBuilder.DefineDynamicModule(moduleName, moduleName, false); - return module; + return assemblyBuilder.DefineDynamicModule(moduleName, moduleName, false); } else -#endif { -#if FEATURE_APPDOMAIN - var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( - assemblyName, AssemblyBuilderAccess.Run); + return assemblyBuilder.DefineDynamicModule(moduleName); + } #else - var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + return assemblyBuilder.DefineDynamicModule(moduleName); #endif + } - var module = assemblyBuilder.DefineDynamicModule(moduleName); - return module; + internal virtual AssemblyBuilder CreateAssembly(bool signStrongName) + { + var assemblyName = GetAssemblyName(signStrongName); + try + { +#if NET462_OR_GREATER + if (savePhysicalAssembly) + { + return AppDomain.CurrentDomain.DefineDynamicAssembly( + assemblyName, + AssemblyBuilderAccess.RunAndSave, + signStrongName ? StrongNamedModuleDirectory : WeakNamedModuleDirectory); + } + else + { + return AppDomain.CurrentDomain.DefineDynamicAssembly( + assemblyName, + AssemblyBuilderAccess.Run); + } +#else + return AssemblyBuilder.DefineDynamicAssembly( + assemblyName, + AssemblyBuilderAccess.Run); +#endif + } + catch (ArgumentException e) when (signStrongName || e.StackTrace?.Contains("ComputePublicKey") == true) + { + var message = string.Format( + "There was an error creating dynamic assembly for your proxies - you don't have permissions " + + "required to sign the assembly. To workaround it you can enforce generating non-signed assembly " + + "only when creating {0}. Alternatively ensure that your account has all the required permissions.", + GetType()); + throw new ArgumentException(message, e); } } - private AssemblyName GetAssemblyName(bool signStrongName) + internal AssemblyName GetAssemblyName(bool signStrongName) { var assemblyName = new AssemblyName { Name = signStrongName ? strongAssemblyName : weakAssemblyName @@ -372,6 +381,15 @@ private AssemblyName GetAssemblyName(bool signStrongName) return assemblyName; } + internal void ResetModules() + { + lock (moduleLocker) + { + moduleBuilder = null; + moduleBuilderWithStrongName = null; + } + } + #if FEATURE_ASSEMBLYBUILDER_SAVE /// /// Saves the generated assembly with the name and directory information given when this instance was created (or with @@ -539,10 +557,15 @@ public void LoadAssemblyIntoCache(Assembly assembly) } #endif - internal TypeBuilder DefineType(bool inSignedModulePreferably, string name, TypeAttributes flags) + internal virtual TypeBuilder DefineType(bool inSignedModulePreferably, string name, TypeAttributes flags) { var module = ObtainDynamicModule(disableSignedModule == false && inSignedModulePreferably); return module.DefineType(name, flags); } + + internal virtual Type BuildType(TypeBuilder typeBuilder) + { + return typeBuilder.CreateTypeInfo()!; + } } } diff --git a/src/Castle.Core/DynamicProxy/PersistentProxyBuilder.cs b/src/Castle.Core/DynamicProxy/PersistentProxyBuilder.cs index 4c591132d..13bc29cd3 100644 --- a/src/Castle.Core/DynamicProxy/PersistentProxyBuilder.cs +++ b/src/Castle.Core/DynamicProxy/PersistentProxyBuilder.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2021 Castle Project - http://www.castleproject.org/ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,12 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -#if FEATURE_ASSEMBLYBUILDER_SAVE - #nullable enable namespace Castle.DynamicProxy { + using System; + using System.Reflection; + +#if NET462_OR_GREATER + /// /// ProxyBuilder that persists the generated type. /// @@ -46,6 +49,40 @@ public PersistentProxyBuilder() : base(new ModuleScope(true)) return ModuleScope.SaveAssembly(); } } -} -#endif \ No newline at end of file +#elif NET9_0_OR_GREATER + + /// + /// that allows you to persist proxy types. + /// Each generated proxy type will be placed in its own separate assembly. + /// + public sealed class PersistentProxyBuilder : DefaultProxyBuilder + { + /// + /// Initializes a new instance of the class. + /// + public PersistentProxyBuilder() + : this(new PersistentProxyBuilderModuleScope()) + { + } + + private PersistentProxyBuilder(PersistentProxyBuilderModuleScope scope) + : base(scope) + { + scope.AssemblyCreated += OnAssemblyCreated; + } + + /// + /// Raised when a new proxy type assembly has been created and loaded into the runtime. + /// + public event EventHandler? AssemblyCreated; + + private void OnAssemblyCreated(Assembly assembly, byte[] assemblyBytes) + { + AssemblyCreated?.Invoke(this, new PersistentProxyBuilderAssemblyEventArgs(assembly, assemblyBytes)); + } + } + +#endif + +} \ No newline at end of file diff --git a/src/Castle.Core/DynamicProxy/PersistentProxyBuilderAssemblyEventArgs.cs b/src/Castle.Core/DynamicProxy/PersistentProxyBuilderAssemblyEventArgs.cs new file mode 100644 index 000000000..d37bbaea2 --- /dev/null +++ b/src/Castle.Core/DynamicProxy/PersistentProxyBuilderAssemblyEventArgs.cs @@ -0,0 +1,53 @@ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if NET9_0_OR_GREATER + +#nullable enable + +namespace Castle.DynamicProxy +{ + using System; + using System.Reflection; + + /// + /// Provides data for the event. + /// + public sealed class PersistentProxyBuilderAssemblyEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The assembly that has been created and loaded into the runtime. + /// The raw bytes of the created assembly (can be saved as a DLL file). + internal PersistentProxyBuilderAssemblyEventArgs(Assembly assembly, byte[] assemblyBytes) + { + Assembly = assembly; + AssemblyBytes = assemblyBytes; + } + + /// + /// The assembly that has been created and loaded into the runtime. + /// + public Assembly Assembly { get; } + + /// + /// The raw bytes of the created assembly (can be saved as a DLL file). + /// + /// + public byte[] AssemblyBytes { get; } + } +} + +#endif diff --git a/src/Castle.Core/DynamicProxy/PersistentProxyBuilderModuleScope.cs b/src/Castle.Core/DynamicProxy/PersistentProxyBuilderModuleScope.cs new file mode 100644 index 000000000..bf514c875 --- /dev/null +++ b/src/Castle.Core/DynamicProxy/PersistentProxyBuilderModuleScope.cs @@ -0,0 +1,118 @@ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if NET9_0_OR_GREATER + +#nullable enable + +namespace Castle.DynamicProxy +{ + using System; + using System.IO; + using System.Reflection; + using System.Reflection.Emit; + using System.Runtime.Loader; + + /// + /// A specialization used by , + /// based on . + /// + /// + /// Dynamic types created with cannot be activated. + /// In order for them to be usable, the dynamic assembly has to first be written out + /// and then loaded by the runtime as a regular assembly. This implies that either all proxy types + /// must be generated ahead of time (if they are to be placed in a single assembly); or, that + /// every proxy type gets its own assembly (if they should be immediately activatable). + /// This class opts for the latter approach. + /// + internal sealed class PersistentProxyBuilderModuleScope : ModuleScope + { + private bool usingStrongNamedModule; + + public PersistentProxyBuilderModuleScope() + : base(savePhysicalAssembly: false, disableSignedModule: false) + { + usingStrongNamedModule = false; + } + + public event Action? AssemblyCreated; + + internal override AssemblyBuilder CreateAssembly(bool signStrongName) + { + var assemblyName = GetAssemblyName(signStrongName); + return new PersistedAssemblyBuilder(assemblyName, coreAssembly: typeof(object).Assembly); + } + + internal override TypeBuilder DefineType(bool inSignedModulePreferably, string name, TypeAttributes flags) + { + TypeBuilder typeBuilder; + + if (IsAuxiliaryType(name) == false) + { + // The requested `TypeBuilder` is for a main proxy type. + // Each of those gets placed in its own assembly. + ResetModules(); + typeBuilder = base.DefineType(inSignedModulePreferably, name, flags); + usingStrongNamedModule = typeBuilder.Module == StrongNamedModule; + } + else + { + // The requested `TypeBuilder` is for an extra type needed for + // the main proxy type. (Currently, those are always invocation types.) + // They need to be placed in the same assembly as the main proxy type, + // otherwise the runtime won't find them during actual use. + // + // TODO: Currently, strong-named modules seem to be preferred for + // invocation types even when the main proxy type is in a non-strong-named + // module. Should find out why that is. Also, ignoring that preference + // may cause other problems. + typeBuilder = base.DefineType(usingStrongNamedModule, name, flags); + } + + return typeBuilder; + } + + internal override Type BuildType(TypeBuilder typeBuilder) + { + Type type = typeBuilder.CreateTypeInfo(); + var typeName = type.FullName!; + + if (IsAuxiliaryType(typeName) == false) + { + var assemblyBuilder = (PersistedAssemblyBuilder)typeBuilder.Assembly; + + using var stream = new MemoryStream(); + assemblyBuilder.Save(stream); + + stream.Seek(0, SeekOrigin.Begin); + var alc = new AssemblyLoadContext(name: null); + var assembly = alc.LoadFromStream(stream); + + stream.Seek(0, SeekOrigin.Begin); + AssemblyCreated?.Invoke(assembly, stream.GetBuffer()); + + type = assembly.GetType(typeName)!; + } + + return type; + } + + private bool IsAuxiliaryType(string name) + { + return name.StartsWith("Castle.Proxies.Invocations."); + } + } +} + +#endif