Skip to content

Commit e14b6bd

Browse files
committed
Implement PersistentProxyBuilder for .NET 9+
1 parent 1963e6e commit e14b6bd

3 files changed

Lines changed: 189 additions & 5 deletions

File tree

src/Castle.Core/DynamicProxy/ModuleScope.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,15 @@ internal AssemblyName GetAssemblyName(bool signStrongName)
377377
return assemblyName;
378378
}
379379

380+
internal void ResetModules()
381+
{
382+
lock (moduleLocker)
383+
{
384+
moduleBuilder = null;
385+
moduleBuilderWithStrongName = null;
386+
}
387+
}
388+
380389
#if FEATURE_ASSEMBLYBUILDER_SAVE
381390
/// <summary>
382391
/// Saves the generated assembly with the name and directory information given when this <see cref = "ModuleScope" /> instance was created (or with
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright 2004-2025 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.IO;
23+
using System.Reflection;
24+
using System.Reflection.Emit;
25+
using System.Runtime.Loader;
26+
27+
/// <summary>
28+
/// A <see cref="ModuleScope"/> specialization that is based on <see cref="PersistedAssemblyBuilder"/>.
29+
/// </summary>
30+
/// <remarks>
31+
/// Dynamic types created with <see cref="PersistedAssemblyBuilder"/> cannot be activated.
32+
/// In order for them to be usable, the dynamic assembly has to first be written out
33+
/// and then loaded by the runtime as a regular assembly. This implies that either all proxy types
34+
/// must be generated ahead of time (if they are to be placed in a single assembly); or, that
35+
/// every proxy type gets its own assembly (if they should be immediately activatable).
36+
/// This class opts for the latter approach, thus its name.
37+
/// </remarks>
38+
internal sealed class PersistentIsolatedTypesModuleScope : ModuleScope
39+
{
40+
private readonly bool savePhysicalAssemblies;
41+
private readonly string outputDirectory;
42+
43+
private int assemblyCount;
44+
private bool usingStrongNamedModule;
45+
46+
public PersistentIsolatedTypesModuleScope(bool savePhysicalAssemblies = true,
47+
string ? outputDirectory = null)
48+
: base(savePhysicalAssembly: false, disableSignedModule: false)
49+
{
50+
this.savePhysicalAssemblies = savePhysicalAssemblies;
51+
this.outputDirectory = outputDirectory ?? Directory.GetCurrentDirectory();
52+
53+
assemblyCount = 0;
54+
usingStrongNamedModule = false;
55+
}
56+
57+
internal override AssemblyBuilder CreateAssembly(bool signStrongName)
58+
{
59+
var assemblyName = GetAssemblyName(signStrongName);
60+
return new PersistedAssemblyBuilder(assemblyName, coreAssembly: typeof(object).Assembly);
61+
}
62+
63+
internal override TypeBuilder DefineType(bool inSignedModulePreferably, string name, TypeAttributes flags)
64+
{
65+
TypeBuilder typeBuilder;
66+
67+
if (IsAuxiliaryType(name) == false)
68+
{
69+
// The requested `TypeBuilder` is for a main proxy type.
70+
// Each of those gets placed in its own assembly.
71+
ResetModules();
72+
typeBuilder = base.DefineType(inSignedModulePreferably, name, flags);
73+
usingStrongNamedModule = typeBuilder.Module == StrongNamedModule;
74+
}
75+
else
76+
{
77+
// The requested `TypeBuilder` is for an extra type needed for
78+
// the main proxy type. (Currently, those are always invocation types.)
79+
// They need to be placed in the same assembly as the main proxy type,
80+
// otherwise the runtime won't find them during actual use.
81+
//
82+
// TODO: Currently, strong-named modules seem to be preferred for
83+
// invocation types even when the main proxy type is in a non-strong-named
84+
// module. Should find out why that is. Also, ignoring that preference
85+
// may cause other problems.
86+
typeBuilder = base.DefineType(usingStrongNamedModule, name, flags);
87+
}
88+
89+
return typeBuilder;
90+
}
91+
92+
internal override Type BuildType(TypeBuilder typeBuilder)
93+
{
94+
Type type = typeBuilder.CreateTypeInfo();
95+
var typeName = type.FullName!;
96+
97+
if (IsAuxiliaryType(typeName) == false)
98+
{
99+
assemblyCount++;
100+
101+
var assemblyBuilder = (PersistedAssemblyBuilder)typeBuilder.Assembly;
102+
103+
using var stream = new MemoryStream();
104+
assemblyBuilder.Save(stream);
105+
106+
if (savePhysicalAssemblies)
107+
{
108+
var inStrongNamedModule = type.Module == StrongNamedModule;
109+
var assemblyFileName = inStrongNamedModule ? StrongNamedModuleName : WeakNamedModuleName;
110+
assemblyFileName = Path.ChangeExtension(
111+
assemblyFileName,
112+
string.Concat(".", assemblyCount.ToString(), Path.GetExtension(assemblyFileName)));
113+
var assemblyPath = Path.Combine(outputDirectory, assemblyFileName);
114+
115+
using var file = File.Open(assemblyPath, FileMode.Create, FileAccess.Write);
116+
stream.Seek(0, SeekOrigin.Begin);
117+
stream.CopyTo(file);
118+
file.Close();
119+
}
120+
121+
stream.Seek(0, SeekOrigin.Begin);
122+
var alc = new AssemblyLoadContext(name: null);
123+
var runnableAssembly = alc.LoadFromStream(stream);
124+
125+
type = runnableAssembly.GetType(typeName)!;
126+
}
127+
128+
return type;
129+
}
130+
131+
private bool IsAuxiliaryType(string name)
132+
{
133+
return name.StartsWith("Castle.Proxies.Invocations.");
134+
}
135+
}
136+
}
137+
138+
#endif

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-2025 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

17+
using System.Diagnostics.CodeAnalysis;
18+
1919
namespace Castle.DynamicProxy
2020
{
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 saves assemblies for the generated types to disk.
57+
/// Each generated proxy type will be placed in its own separate assembly.
58+
/// </summary>
59+
/// <remarks>
60+
/// <para>
61+
/// Each assembly will be automatically saved when the proxy type is created.
62+
/// Saved assemblies include a generation number in their file names
63+
/// (e. g. <c>CastleDynProxy2.{1,2,3,...}.dll</c>) so previous assemblies
64+
/// aren't overwritten.
65+
/// </para>
66+
/// <para>
67+
/// <b>Use with caution!</b> This class may lead to a lot of files being written to disk.
68+
/// There may also be some overhead in the runtime due to the larger number of assemblies.
69+
/// </para>
70+
/// </remarks>
71+
public sealed class PersistentProxyBuilder : DefaultProxyBuilder
72+
{
73+
/// <summary>
74+
/// Initializes a new instance of the <see cref = "PersistentProxyBuilder" /> class.
75+
/// </summary>
76+
/// <param name="outputDirectory">
77+
/// The directory where assemblies will be saved.
78+
/// If <see langword="null"/>, the current working directory gets used.
79+
/// </param>
80+
public PersistentProxyBuilder(string? outputDirectory = null)
81+
: base(new PersistentIsolatedTypesModuleScope(savePhysicalAssemblies: true, outputDirectory))
82+
{
83+
}
84+
}
85+
86+
#endif
87+
88+
}

0 commit comments

Comments
 (0)