Im modernen .NET konnten zur Laufzeit erzeugte Assemblies bisher nur im RAM gehalten werden. Mit .NET 9.0 ist es nun auch wieder wie im klassischen .NET Framework möglich, zur Laufzeit erzeugte Assemblies im Dateisystem zu persistieren.
Dr. Holger Schwichtenberg ist technischer Leiter des Expertennetzwerks www.IT-Visions.de, das mit 53 renommierten Experten zahlreiche mittlere und große Unternehmen durch Beratungen und Schulungen sowie bei der Softwareentwicklung unterstützt. Durch seine Auftritte auf zahlreichen nationalen und internationalen Fachkonferenzen sowie mehr als 90 Fachbücher und mehr als 1500 Fachartikel gehört Holger Schwichtenberg zu den bekanntesten Experten für .NET und Webtechniken in Deutschland.
In .NET 9.0 Preview 1 hatte Microsoft die aus dem klassischen .NET Framework bekannte Möglichkeit wieder eingeführt, dynamisch zur Laufzeit erstellte Assemblies im Dateisystem oder einem beliebigen Stream zu persistieren.
In Preview 3 änderte Microsoft die API aber erneut: Anstelle der zuvor verwendeten Klasse AssemblyBuilder
AssemblyBuilder ab = AssemblyBuilder.DefinePersistedAssembly(new AssemblyName("Math"), typeof(object).Assembly);nutzt man nun die neue Klasse PersistedAssemblyBuilder:
PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("Math"), typeof(object).Assembly);Folgender Code zeigt den Einsatz der neuen Klasse. Weitere Beispiele zur Anpassung der Metadaten wie dem Einstiegspunkt finden sich in den Release Notes.
using System.Reflection; using System.Reflection.Emit; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; using ITVisions; using Microsoft.NET.HostModel.AppHost; namespace NET9_Console.FCL90; internal class FCL9_Reflection { /// <summary> /// Im modernen .NET konnten zur Laufzeit erzeugte Assemblies /// bisher nur im RAM gehalten werden. Mit .NET 9.0 ist es mit /// PersistedAssemblyBuilder nun auch wieder möglich, zur Laufzeit /// erzeugte Assemblies im Dateisystem zu persistieren. /// siehe auch https://stackoverflow.com/questions/78466316/unhandled-exception-system-io-filenotfoundexception-the-file-or-assembly-syst /// </summary> public void CreateAndSaveAssembly() { // TODO: Bei RTM-Version anpassen! var currentVersion = "9.0.0"; CUI.Demo(nameof(CreateAndSaveAssembly)); string AssemblyName = "App42"; string AssemblyNameWithExtension = AssemblyName + ".exe"; CUI.H2($"\nErzeuge {AssemblyNameWithExtension}"); string referencePath = @$"C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\{currentVersion}\ref\net9.0"; PathAssemblyResolver resolver = new(Directory.GetFiles(referencePath, "*.dll")); using MetadataLoadContext context = new(resolver); Assembly coreAssembly = context.CoreAssembly!; Type voidType = coreAssembly.GetType(typeof(void).FullName!)!; Type objectType = coreAssembly.GetType(typeof(object).FullName!)!; Type stringType = coreAssembly.GetType(typeof(string).FullName!)!; Type int32Type = coreAssembly.GetType(typeof(Int32).FullName!)!; Type stringArrayType = coreAssembly.GetType(typeof(string[]).FullName!)!; Type consoleType = coreAssembly.GetType(typeof(Console).FullName!)!; // --> NEU in .NET 9.0: PersistedAssemblyBuilder // https://learn.microsoft.com/en-us/dotnet/api/system.reflection.emit.persistedassemblybuilder?view=net-9.0 PersistedAssemblyBuilder assemblyBuilder = new(new AssemblyName(AssemblyName), coreAssembly); TypeBuilder typeBuilder = assemblyBuilder.DefineDynamicModule(AssemblyName).DefineType(AssemblyName, TypeAttributes.Public | TypeAttributes.Class, objectType); #region Methode Sum() erzeugen var mb = typeBuilder.DefineMethod("Sum", MethodAttributes.Public | MethodAttributes.Static, int32Type, new Type[] { int32Type, int32Type }); var ilSum = mb.GetILGenerator(); ilSum.Emit(OpCodes.Ldarg_0); ilSum.Emit(OpCodes.Ldarg_1); ilSum.Emit(OpCodes.Add); ilSum.Emit(OpCodes.Ret); CUI.Cyan($"Sum() wurde erzeugt"); #endregion #region Methode Main() erzeugen MethodBuilder methodBuilder = typeBuilder.DefineMethod("Main", MethodAttributes.Public | MethodAttributes.Static, voidType, [stringArrayType]); ILGenerator ilMain = methodBuilder.GetILGenerator(); // Aufruf von Console.WriteLine("Die Antwort auf Ihre Frage ist:") ilMain.Emit(OpCodes.Ldstr, "Die Antwort auf Ihre Frage ist:"); ilMain.Emit(OpCodes.Call, consoleType.GetMethod("WriteLine", [stringType])!); // Aufruf von Console.WriteLine(Sum(40,2)) ilMain.Emit(OpCodes.Ldc_I4, 40); // Load the constant 40 ilMain.Emit(OpCodes.Ldc_I4, 2); // Load the constant 2 ilMain.Emit(OpCodes.Call, mb); var writeLineMethod = consoleType.GetMethod("WriteLine", [int32Type]); ilMain.Emit(OpCodes.Call, writeLineMethod); ilMain.Emit(OpCodes.Ret); CUI.Cyan($"Main() wurde erzeugt"); #endregion #region Metadaten und PE-Header erzeugen typeBuilder.CreateType(); MetadataBuilder metadataBuilder = assemblyBuilder.GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder fieldData); PEHeaderBuilder peHeaderBuilder = new(imageCharacteristics: Characteristics.ExecutableImage); ManagedPEBuilder peBuilder = new( header: peHeaderBuilder, metadataRootBuilder: new MetadataRootBuilder(metadataBuilder), ilStream: ilStream, mappedFieldData: fieldData, entryPoint: MetadataTokens.MethodDefinitionHandle(methodBuilder.MetadataToken)); CUI.Cyan($"Metadaten und PE-Header wurden erzeugt"); #endregion #region Speichern der DLL BlobBuilder peBlob = new(); peBuilder.Serialize(peBlob); using (FileStream fileStream = new($"{AssemblyName}.dll", FileMode.Create, FileAccess.Write)) { peBlob.WriteContentTo(fileStream); } CUI.Cyan($"{AssemblyName}.dll wurde gespeichert"); #endregion #region AppHost und runtimeconfig erzeugen HostWriter.CreateAppHost( @$"C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Host.win-x64\{currentVersion}\runtimes\win-x64\native\apphost.exe", $"{AssemblyName}.exe", $"{AssemblyName}.dll"); CUI.Cyan($"{AssemblyName}.exe wurde gespeichert"); File.WriteAllText($"{AssemblyName}.runtimeconfig.json", $$""" {"runtimeOptions": { "tfm": "net9.0", "framework": { "name": "Microsoft.NETCore.App", "version": "{{currentVersion}}" } } } """); CUI.Cyan($"{AssemblyName}.runtimeconfig.json wurde gespeichert"); #endregion CUI.Success("OK"); #region Testen CUI.H2($"\nStarte {AssemblyName}.exe"); System.Diagnostics.Process.Start($"{AssemblyName}.exe"); #endregion } }Der Code gibt die einzelnen Schritte beim Erzeugen und Persistieren der Assembly aus.
(Bild: Screenshot (Holger Schwichtenberg))
(Bild: coffeemill/123rf.com)
Das nächste LTS-Release steht an: Auf der Online-Konferenz betterCode() .NET 10.0 am 18. November 2025 – ausgerichtet von iX und dpunkt.verlag in Kooperation mit IT-visions.de – präsentieren der Autor dieses Artikels, Dr. Holger Schwichtenberg, und weitere Experten die wichtigsten Neuerungen. Dazu zählen die Updates im .NET 10.0 SDK sowie in C# 14.0, ASP.NET Core 10.0, Blazor 10.0, Windows Forms 10.0, WPF 10.0, WinUI 3, .NET MAUI 10.0 und die Integration von Künstlicher Intelligenz in .NET-Anwendungen.
Das Programm ist noch nicht veröffentlicht – bis dahin sind vergünstigte Blind-Bird-Tickets bereits im Online-Shop erhältlich. Das Vorjahresprogramm lässt sich im Archiv einsehen.
(rme)