Skip to content

Commit 2ecd4ed

Browse files
authored
Merge pull request #718 from stakx/persistent-proxy-builder
Restore ability on .NET 9 and later to save dynamic assemblies to disk
2 parents 6a81dd5 + 737ef78 commit 2ecd4ed

File tree

8 files changed

+343
-46
lines changed

8 files changed

+343
-46
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Breaking Changes:
88

99
Enhancements:
1010
- Minimally improved support for methods having `ref struct` parameter and return types, such as `Span<T>`: 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)
11+
- Restore ability on .NET 9 and later to save dynamic assemblies to disk using `PersistentProxyBuilder` (@stakx, #718)
1112
- Dependencies were updated
1213

1314
Bugfixes:

ref/Castle.Core-net9.0.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2585,6 +2585,16 @@ public ModuleScope(bool savePhysicalAssembly, bool disableSignedModule, string s
25852585
public string WeakNamedModuleName { get; }
25862586
public static byte[] GetKeyPair() { }
25872587
}
2588+
public sealed class PersistentProxyBuilder : Castle.DynamicProxy.DefaultProxyBuilder
2589+
{
2590+
public PersistentProxyBuilder() { }
2591+
public event System.EventHandler<Castle.DynamicProxy.PersistentProxyBuilderAssemblyEventArgs>? AssemblyCreated;
2592+
}
2593+
public sealed class PersistentProxyBuilderAssemblyEventArgs : System.EventArgs
2594+
{
2595+
public System.Reflection.Assembly Assembly { get; }
2596+
public byte[] AssemblyBytes { get; }
2597+
}
25882598
public class ProxyGenerationOptions
25892599
{
25902600
public static readonly Castle.DynamicProxy.ProxyGenerationOptions Default;

src/Castle.Core.Tests/DynamicProxy.Tests/PersistentProxyBuilderTestCase.cs

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2004-2021 Castle Project - http://www.castleproject.org/
1+
// Copyright 2004-2026 Castle Project - http://www.castleproject.org/
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -12,14 +12,20 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
#if FEATURE_ASSEMBLYBUILDER_SAVE
16-
1715
namespace Castle.DynamicProxy.Tests
1816
{
1917
using System;
18+
using System.Collections.Generic;
2019
using System.IO;
20+
using System.Linq;
21+
using System.Reflection;
22+
23+
using Castle.DynamicProxy.Tests.Interfaces;
24+
2125
using NUnit.Framework;
2226

27+
#if NET462_OR_GREATER
28+
2329
[TestFixture]
2430
public class PersistentProxyBuilderTestCase
2531
{
@@ -44,6 +50,55 @@ public void PersistentProxyBuilder_SavesSignedFile()
4450
Assert.IsTrue(path.EndsWith(ModuleScope.DEFAULT_FILE_NAME));
4551
}
4652
}
47-
}
4853

49-
#endif
54+
#elif NET9_0_OR_GREATER
55+
56+
[TestFixture]
57+
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
58+
public class PersistentProxyBuilderTestCase
59+
{
60+
private List<Assembly> assemblies;
61+
private PersistentProxyBuilder builder;
62+
63+
[SetUp]
64+
public void SetupProxyBuilder()
65+
{
66+
assemblies = new List<Assembly>();
67+
builder = new PersistentProxyBuilder();
68+
builder.AssemblyCreated += (object _, PersistentProxyBuilderAssemblyEventArgs e) =>
69+
{
70+
assemblies.Add(e.Assembly);
71+
};
72+
}
73+
74+
[Test]
75+
public void SavesOneAssemblyPerProxiedType()
76+
{
77+
var oneProxyType = builder.CreateInterfaceProxyTypeWithoutTarget(typeof(IOne), Type.EmptyTypes, ProxyGenerationOptions.Default);
78+
Assert.AreEqual(1, assemblies.Count);
79+
80+
var twoProxyType = builder.CreateInterfaceProxyTypeWithoutTarget(typeof(ITwo), Type.EmptyTypes, ProxyGenerationOptions.Default);
81+
Assert.AreEqual(2, assemblies.Count);
82+
83+
var oneAssembly = assemblies[0];
84+
var twoAssembly = assemblies[1];
85+
Assert.AreSame(oneAssembly, oneProxyType.Assembly);
86+
Assert.AreSame(twoAssembly, twoProxyType.Assembly);
87+
Assert.AreNotSame(oneAssembly, twoAssembly);
88+
}
89+
90+
[Test]
91+
public void TypeCacheWorks()
92+
{
93+
var proxyType1 = builder.CreateClassProxyType(typeof(object), Type.EmptyTypes, ProxyGenerationOptions.Default);
94+
var proxyType2 = builder.CreateClassProxyType(typeof(object), Type.EmptyTypes, ProxyGenerationOptions.Default);
95+
96+
Assert.AreEqual(1, assemblies.Count);
97+
Assert.AreSame(proxyType1, proxyType2);
98+
Assert.AreSame(proxyType1.Assembly, proxyType2.Assembly);
99+
}
100+
}
101+
102+
#endif
103+
104+
}

src/Castle.Core/DynamicProxy/Generators/Emitters/ClassEmitter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2004-2025 Castle Project - http://www.castleproject.org/
1+
// Copyright 2004-2026 Castle Project - http://www.castleproject.org/
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -412,7 +412,7 @@ public Type BuildType()
412412
builder.Generate();
413413
}
414414

415-
var type = typeBuilder.CreateTypeInfo();
415+
var type = moduleScope.BuildType(typeBuilder);
416416

417417
return type;
418418
}

src/Castle.Core/DynamicProxy/ModuleScope.cs

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2004-2021 Castle Project - http://www.castleproject.org/
1+
// Copyright 2004-2026 Castle Project - http://www.castleproject.org/
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -311,50 +311,59 @@ internal ModuleBuilder ObtainDynamicModuleWithWeakName()
311311

312312
private ModuleBuilder CreateModule(bool signStrongName)
313313
{
314-
var assemblyName = GetAssemblyName(signStrongName);
314+
var assemblyBuilder = CreateAssembly(signStrongName);
315315
var moduleName = signStrongName ? StrongNamedModuleName : WeakNamedModuleName;
316-
#if FEATURE_APPDOMAIN
316+
#if NET462_OR_GREATER
317317
if (savePhysicalAssembly)
318318
{
319-
AssemblyBuilder assemblyBuilder;
320-
try
321-
{
322-
assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
323-
assemblyName, AssemblyBuilderAccess.RunAndSave, signStrongName ? StrongNamedModuleDirectory : WeakNamedModuleDirectory);
324-
}
325-
catch (ArgumentException e)
326-
{
327-
if (signStrongName == false && e.StackTrace.Contains("ComputePublicKey") == false)
328-
{
329-
// I have no idea what that could be
330-
throw;
331-
}
332-
var message = string.Format(
333-
"There was an error creating dynamic assembly for your proxies - you don't have permissions " +
334-
"required to sign the assembly. To workaround it you can enforce generating non-signed assembly " +
335-
"only when creating {0}. Alternatively ensure that your account has all the required permissions.",
336-
GetType());
337-
throw new ArgumentException(message, e);
338-
}
339-
var module = assemblyBuilder.DefineDynamicModule(moduleName, moduleName, false);
340-
return module;
319+
return assemblyBuilder.DefineDynamicModule(moduleName, moduleName, false);
341320
}
342321
else
343-
#endif
344322
{
345-
#if FEATURE_APPDOMAIN
346-
var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
347-
assemblyName, AssemblyBuilderAccess.Run);
323+
return assemblyBuilder.DefineDynamicModule(moduleName);
324+
}
348325
#else
349-
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
326+
return assemblyBuilder.DefineDynamicModule(moduleName);
350327
#endif
328+
}
351329

352-
var module = assemblyBuilder.DefineDynamicModule(moduleName);
353-
return module;
330+
internal virtual AssemblyBuilder CreateAssembly(bool signStrongName)
331+
{
332+
var assemblyName = GetAssemblyName(signStrongName);
333+
try
334+
{
335+
#if NET462_OR_GREATER
336+
if (savePhysicalAssembly)
337+
{
338+
return AppDomain.CurrentDomain.DefineDynamicAssembly(
339+
assemblyName,
340+
AssemblyBuilderAccess.RunAndSave,
341+
signStrongName ? StrongNamedModuleDirectory : WeakNamedModuleDirectory);
342+
}
343+
else
344+
{
345+
return AppDomain.CurrentDomain.DefineDynamicAssembly(
346+
assemblyName,
347+
AssemblyBuilderAccess.Run);
348+
}
349+
#else
350+
return AssemblyBuilder.DefineDynamicAssembly(
351+
assemblyName,
352+
AssemblyBuilderAccess.Run);
353+
#endif
354+
}
355+
catch (ArgumentException e) when (signStrongName || e.StackTrace?.Contains("ComputePublicKey") == true)
356+
{
357+
var message = string.Format(
358+
"There was an error creating dynamic assembly for your proxies - you don't have permissions " +
359+
"required to sign the assembly. To workaround it you can enforce generating non-signed assembly " +
360+
"only when creating {0}. Alternatively ensure that your account has all the required permissions.",
361+
GetType());
362+
throw new ArgumentException(message, e);
354363
}
355364
}
356365

357-
private AssemblyName GetAssemblyName(bool signStrongName)
366+
internal AssemblyName GetAssemblyName(bool signStrongName)
358367
{
359368
var assemblyName = new AssemblyName {
360369
Name = signStrongName ? strongAssemblyName : weakAssemblyName
@@ -372,6 +381,15 @@ private AssemblyName GetAssemblyName(bool signStrongName)
372381
return assemblyName;
373382
}
374383

384+
internal void ResetModules()
385+
{
386+
lock (moduleLocker)
387+
{
388+
moduleBuilder = null;
389+
moduleBuilderWithStrongName = null;
390+
}
391+
}
392+
375393
#if FEATURE_ASSEMBLYBUILDER_SAVE
376394
/// <summary>
377395
/// Saves the generated assembly with the name and directory information given when this <see cref = "ModuleScope" /> instance was created (or with
@@ -539,10 +557,15 @@ public void LoadAssemblyIntoCache(Assembly assembly)
539557
}
540558
#endif
541559

542-
internal TypeBuilder DefineType(bool inSignedModulePreferably, string name, TypeAttributes flags)
560+
internal virtual TypeBuilder DefineType(bool inSignedModulePreferably, string name, TypeAttributes flags)
543561
{
544562
var module = ObtainDynamicModule(disableSignedModule == false && inSignedModulePreferably);
545563
return module.DefineType(name, flags);
546564
}
565+
566+
internal virtual Type BuildType(TypeBuilder typeBuilder)
567+
{
568+
return typeBuilder.CreateTypeInfo()!;
569+
}
547570
}
548571
}

src/Castle.Core/DynamicProxy/PersistentProxyBuilder.cs

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2004-2021 Castle Project - http://www.castleproject.org/
1+
// Copyright 2004-2026 Castle Project - http://www.castleproject.org/
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -12,12 +12,15 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
#if FEATURE_ASSEMBLYBUILDER_SAVE
16-
1715
#nullable enable
1816

1917
namespace Castle.DynamicProxy
2018
{
19+
using System;
20+
using System.Reflection;
21+
22+
#if NET462_OR_GREATER
23+
2124
/// <summary>
2225
/// ProxyBuilder that persists the generated type.
2326
/// </summary>
@@ -46,6 +49,40 @@ public PersistentProxyBuilder() : base(new ModuleScope(true))
4649
return ModuleScope.SaveAssembly();
4750
}
4851
}
49-
}
5052

51-
#endif
53+
#elif NET9_0_OR_GREATER
54+
55+
/// <summary>
56+
/// <see cref="IProxyBuilder"/> that allows you to persist proxy types.
57+
/// Each generated proxy type will be placed in its own separate assembly.
58+
/// </summary>
59+
public sealed class PersistentProxyBuilder : DefaultProxyBuilder
60+
{
61+
/// <summary>
62+
/// Initializes a new instance of the <see cref = "PersistentProxyBuilder" /> class.
63+
/// </summary>
64+
public PersistentProxyBuilder()
65+
: this(new PersistentProxyBuilderModuleScope())
66+
{
67+
}
68+
69+
private PersistentProxyBuilder(PersistentProxyBuilderModuleScope scope)
70+
: base(scope)
71+
{
72+
scope.AssemblyCreated += OnAssemblyCreated;
73+
}
74+
75+
/// <summary>
76+
/// Raised when a new proxy type assembly has been created and loaded into the runtime.
77+
/// </summary>
78+
public event EventHandler<PersistentProxyBuilderAssemblyEventArgs>? AssemblyCreated;
79+
80+
private void OnAssemblyCreated(Assembly assembly, byte[] assemblyBytes)
81+
{
82+
AssemblyCreated?.Invoke(this, new PersistentProxyBuilderAssemblyEventArgs(assembly, assemblyBytes));
83+
}
84+
}
85+
86+
#endif
87+
88+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2004-2026 Castle Project - http://www.castleproject.org/
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#if NET9_0_OR_GREATER
16+
17+
#nullable enable
18+
19+
namespace Castle.DynamicProxy
20+
{
21+
using System;
22+
using System.Reflection;
23+
24+
/// <summary>
25+
/// Provides data for the <see cref="PersistentProxyBuilder.AssemblyCreated"/> event.
26+
/// </summary>
27+
public sealed class PersistentProxyBuilderAssemblyEventArgs : EventArgs
28+
{
29+
/// <summary>
30+
/// Initializes a new instance of the <see cref="PersistentProxyBuilderAssemblyEventArgs"/> class.
31+
/// </summary>
32+
/// <param name="assembly">The assembly that has been created and loaded into the runtime.</param>
33+
/// <param name="assemblyBytes">The raw bytes of the created assembly (can be saved as a DLL file).</param>
34+
internal PersistentProxyBuilderAssemblyEventArgs(Assembly assembly, byte[] assemblyBytes)
35+
{
36+
Assembly = assembly;
37+
AssemblyBytes = assemblyBytes;
38+
}
39+
40+
/// <summary>
41+
/// The assembly that has been created and loaded into the runtime.
42+
/// </summary>
43+
public Assembly Assembly { get; }
44+
45+
/// <summary>
46+
/// The raw bytes of the created assembly (can be saved as a DLL file).
47+
/// </summary>
48+
/// <remarks></remarks>
49+
public byte[] AssemblyBytes { get; }
50+
}
51+
}
52+
53+
#endif

0 commit comments

Comments
 (0)