Modernisierung mit Code Multi-Targeting

Autor
Rolf Wenger
Thema
Softwareentwicklung
Lesezeit
15-20 Minuten

Abstrakt

Mit der Einführung des .NET Frameworks hat Microsoft in den frühen 2000er Jahren definiert, dass ein API (Application Programming Interface, die System-Programmierschnittstelle für Anwendungsentwicklungen) zirka alle 10 Jahre neu entwickelt werden muss. Gemessen an der .NET Technologie hat Microsoft, diese eigene Vorgabe ab 2016 wiederum umgesetzt und das .NET Framework einer Komplettüberarbeitung unterzogen und mit der Einführung von .NET Core (heute schlicht nur noch .NET genannt) wiederholt. Zwar ist die neue .NET Version in hohem Masse zur Vorgängerversion kompatibel, aber eben nicht komplett kompatibel.

Wenn umfangreiche Anwendungen wie das Client-Server-Framework der weroSoft AG modernisiert und für den Einsatz mit den neusten .NET Versionen von Microsoft verfügbar werden sollen, hat das durchaus seine Tücken. Dieser Artikel beschreibt das Szenario der Modernisierung von Software und seine Lösungsansätze in Bezug auf die Verwendung der unterliegenden Frameworks wie Microsoft’s .NET.

Beschreibung der Herausforderung

Mit dem Entscheid das Microsoft .NET Framework und darauf aufbauende Bibliotheken von Drittherstellern für den Aufbau des eigenen Anwendungs-Frameworks zu verwenden, übernahmen wir vor 15 Jahren auch die Verantwortung für unsere Kunden den Aufbau der Anwendungen zukunftssicher zu machen. Dazu gehört neben der funktionalen Weiterentwicklung natürlich auch die Sicherstellung, das Framework so zu gestalten, dass es für den Einsatz in der sich verändernden Informatik-Landschaft genutzt werden kann. Dabei sind folgende Veränderungen von besonderer Bedeutung:

  • Einsatz in nativer Cloud-Umgebung
  • Containerisierung
  • Betrieb auf LINUX als Basisbetriebssystem
  • Verwendung von simplifizierten Kommunikationsprotokollen

Aufgrund der Marketing-Unterlagen von Microsoft ist das alles einfach so möglich, sofern moderne Anwendungen auf der Basis von .NET aufgebaut werden. Leider ist aber die neue .NET Umgebung aber nicht vollständig kompatibel zum “alten .NET Framework”. Das betrifft im Besonderen folgende Themen (nicht komplette Aufzählung):

  • Teile der grundlegenden Bibliothek sind nicht verfügbar oder nur noch beschränkt verfügbar
    • Application Domain (eingeschränkt)
    • Code Access Security (abgeschafft)
    • Reflektion (vorhanden aber funktioniert teilweise anders)
  • Technologie-spezifische Teile des Frameworks sind nicht für alle Plattformen wie Linux verfügbar
    • Windows Presentation Foundation (WPF)
    • Windows Management Instrumentation (WMI)
    • Windows Communication Foundation (WCF, neu als separate Bibliothek in Entwicklung)
  • Spezielle Zugriffe auf die grundlegende Betriebssystemfunktionalität (P-Invoke)
  • Verändertes Konfigurationsverhalten
  • Portierte Bibliotheken von Drittherstellern sind unter Umständen nicht kompatibel
  • Sobald der Betrieb für UNIX vorgesehen ist, kommen auch viele Windows-spezifische Gesichtspunkte dazu:
    • Standardverzeichnisse für Daten sind anders organisiert
    • Teilfunktionalitäten sind nicht vorhanden (API Aufrufe liefern schlicht kein Resultat)
    • Berechtigungen und Handhabung von Betriebssystemobjekten kann stark abweichen

Mit der obigen Liste standen wir vor vier Jahren vor einer schier unüberwindbaren Hürde und mussten uns einen Weg zurechtlegen, wie wir unser Framework umbauen konnten, so dass wir auf der einen Seite für unsere Kunden einen hohen Grad an Kompatibilität wahren können, auf der andern Seite aber möglichst einen grossen Teil der Investition in die nächste Dekade überführen können.

Lösungsansatz

Zuerst einmal brauchten wir eine konkrete Übersicht der notwendigen Arbeiten und eine konkrete Vorstellung, welche Teile des eigenen Frameworks und der eigenen Produkte in welchem Umfang von Überarbeitungen betroffen waren und welche nicht. Dazu mussten viele Funktionalitäten studiert, Prototyping gemacht und Wege für die Umstellung gesucht werden. Das Ganze hat nicht zuletzt deshalb auch viel Zeit gebraucht, weil Microsoft am Anfang der neuen .NET Welt ein paar “Haken geschlagen” hat, und die neue Technik immer wieder auch grossen Änderungen unterzogen hat. Seit der Version .NET 5 (Ende 2020) ist nun aber ein etablierter Weg vorhanden und die Kompatibilität ist nun sowohl für die Entwicklungswerkzeuge und die Funktionalität stabil und rein vorwärtsorientiert.

Aufgrund der grundlegenden Entwicklung konnten in der Folge folgende Schritte eingeleitet werden:

  • Festlegen der eigenen Strategie
  • Definieren der Funktionalitäten, die aus technischen Gründen eine Komplettüberarbeitung bedürfen
  • Definieren der sanften Überführung des Codes, ohne den Code redundant zu halten

Bei der obigen Definition wurde schnell klar, dass Überführung des gesamten Codes in klaren Schritten unter Wahrung einer hoher Kompatibilität über die Bühne gehen muss. Gleichzeitig müssen wir sicherstellen, dass der Code für die verschiedenen Laufzeitumgebungen der Kundenumgebungen funktionieren muss, denn die installierte Infrastruktur muss auch immer in der Lage sein die verlangen Versionen des Basissystems zu verdauen (sprich installieren). Die neusten .NET Versionen können nur auf der neusten Generation von Windows installiert werden. Das entspricht aber nicht immer den Tatsachen der Kundeninfrastrukturen.

Umsetzung

Für die Umsetzung des Vorhabens haben wir vor ca. zwei Jahren entschieden die Migration des eigenen Codes so zu gestalten, dass wir in der Lage sind auf der Basis von einer Sourcecode-Version unser Framework in mehreren Schritten so zu migrieren, dass mit jedem Schritt mehrere Versionen des Produkts entstehen. Konkret heisst das, dass wir aus dem Sourcecode aktuell folgende Versionen herstellen:

  • Version für .NET Framework 4.7.2 (die maximal kompatible Version zu den Vorgängerversionen)
  • Version für .NET Framework 4.8 (Schrittweise modernisierte Version für den langfristigen Support auf der Basis des .NET Frameworks).
  • Version für .NET 8 (Schrittweise modernisierte Version für den langfristigen, universellen Einsatz auf Windows und Unix-basierten Systemen).

Die obigen Liste lässt vermuten, dass es für die Funktionalitäten durchaus unterschiedliche Situation für die Erstellung des Code gibt:

  • Funktionen können im Ziel 100% umgesetzt werden (zum Glück der grösste Teil)
  • Funktionen müssen intern anders ablaufen
  • Funktionen sind gar nicht vorhanden

Um die obige Liste der Situation für die Codeherstellung umzusetzen, arbeiten wir mit folgenden Hilfsmitteln:

  • Definition und Verwendung von Codeschaltern (auch Präprozessordirektiven genannt). Die Verwendung solcher Schalter erlauben beim Herstellen des Laufzeitcodes, je nach Zielumgebung, den Code unterschiedlich zu kompilieren, obschon es sich um denselben Code handelt.
  • Definition von Multi-Target Projekten. Damit steuern wir die Entwicklungsumgebung so, dass beim Herstellungsprozess aus dem Sourcecode mit einer Herstellung mehrere Zielplattformen adressiert werden können und als Output des Prozesses die drei genannten Versionen direkt entstehen.

Die Verwendung von Präprozessordirektiven

Die Verwendung von Präprozessordirektiven basiert darauf, dass zum einen in der Projektdefinition entsprechende Konstanten definiert und diese dann fallbezogen ausgewertet werden. Wir haben grob folgende Direktiven definiert:

  • IsNetFramework und IsNetCore
    Mit diesen Direktiven markieren wir Code, der nur für das eine oder andere Framework Verwendung findet. Ist Code nicht gekennzeichnet, ist er für beide Frameworks zu verwenden. Letzteres trifft zum Glück für den grössten Teil des Codes zu (> 95%).
  • Generation2 und Generation3
    Mit diesen Direktiven markieren wir Code, der innerhalb des eigenen Frameworks eine komplette Überarbeitung erfährt und unabhängig vom Zielframework in eine neue Generation überführt wird.

Das untenstehende Codefragment gibt einer Vorstellung wie eine Direktive im Code angewendet wird. Beachte, dass die Methode intern eine Kapselung darstellt und die “Weichenfunktion” so intern versteckt wird, was wiederum die Code-Wartung und die Testbarkeit erhöht. Auf Zeile 18 ist die eigentliche Weiche vorhanden die bewirkt, dass der Code Zeile 23 im Fall von .NET Framework und der Code Zeile 29 im Fall von .NET kompiliert wird. Die Steuerung der Version wird über die Projektdatei vorgenommen, was weiter unten im Artikel erklärt wird. Beachte, dass der aufgerufene Code in separaten Klassen implementiert ist. Das erhöht in diesem Fall ebenfalls die Wartbarkeit und die Testbarkeit.

  /// <summary>
  /// Loads the reflection info from the installed assemblies.
  /// </summary>
  /// <returns>The process reflection info.</returns>
  private ProcessReflectionInfo LoadReflectionInfo()
  {
      // Get all Assemblies by their file name and create the requested scan definition afterwards.
      IEnumerable<string> assemblyFileNames = AssemblyScanHelper.GetAllAssemblyFileNames(IncludeFilePatterns, ExcludeFilePatterns);
      AssemblyScanDefinition scanDefinition = new AssemblyScanDefinition
      {
         AssemblyFileNames = assemblyFileNames.ToList(),
         LoadAbstractTypes = LoadAbstractTypes,
         LoadAttributes = LoadAttributes,
         LoadEnumerations = LoadEnumerations,
         LoadInterfaces = LoadInterfaces
      };

#if IsNetFramework
      // -----------------------------------------------------------------------------------------------------
      // This code part implements loading the reflection info from with the .NET Framework. Changes at
      // this point must be considered to be applied to the .NET Core implementation too.
      // -----------------------------------------------------------------------------------------------------
      return AssemblyScannerNetFramework.LoadReflectionInfoNetFramework(scanDefinition);
#else
      // -----------------------------------------------------------------------------------------------------
      // This code part implements loading the reflection info from with the .NET Core. Changes at
      // this point must be considered to be applied to the .NET Framework implementation too.
      // -----------------------------------------------------------------------------------------------------
      return AssemblyScannerNetCore.LoadReflectionInfoNetFramework(scanDefinition);
#endif
   }

Die Definition von Multi-Target-Projekten

Mit der Einführung von Visual Studio 2017 hat Microsoft auch die Unterstützung von so genannten SDK-basierten Projektdateien definiert. Dieser Typ von Projektdateien ist die Grundlage für die Erstellung von Projekten, die einer mehrfachen gleichzeitigen Kompilation unterliegen. Wir müssen also auch die Projektdateien umstellen, um unser Ziel zu erreichen, was bei mehr als 400 Dateien eine Aufgabe in der Aufgabe ist. Mit der Einführung von Multi-Target müssen auch vielfältige Anpassungen gemacht werden, die den unterschiedlichen Definitionen für die unterschiedlichen Code- und Zielplattform-Konstellationen Rechnung tragen.

Die Definition des Multi-Target geschieht, indem in der Projektdefinitionsdatei die Pluralform das Tag <TargetFramework> verwendet wird. Diese Form erlaubt die Definition beliebig vieler (1…n) Zielplattformen, für die MS-Build (oder Visual Studio). Im Beispiel unten wird MS-Build angewiesen, das Projekt dreimal zu kompilieren. Einmal für .NET 8, einmal für .NET Framework 4.8 und einmal für .NET Framework 4.7.2. beachte, dass die verwendeten Kürzel von Microsoft vordefiniert sind und im Einklang mit dem Projekt SDK Typ im Einklang sein müssen.

<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net8.0;net48;net472</TargetFrameworks>
...

Mit der alleinigen Definition des Multi-Target ist die Projektdatei aber noch nicht fertig. Je nachdem wie und was in der Codeherstellung noch weiter spezifisch zum aktuell gerade kompilierten Projekttyp anders ablaufen muss, müssen Einstellungen vorgenommen werden. Das Beispiel zeigt die im Zusammenhang mit den Präprozessordirektiven erwähnten Schalter für die Beeinflussung des Compilers für alle drei Zielplattformen für Debug und Release Build.

<!-- Define switches for build control *** DEBUG *** -->
  <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0|AnyCPU'">
    <DocumentationFile>bin\Debug\net8.0\WeroSoft.Core.Library.xml</DocumentationFile>
    <DefineConstants>$(DefineConstants);Generation3</DefineConstants>
    <DefineConstants>$(DefineConstants);IsNetCore</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net48|AnyCPU'">
    <DocumentationFile>bin\Debug\net48\WeroSoft.Core.Library.xml</DocumentationFile>
    <DefineConstants>$(DefineConstants);Generation3</DefineConstants>
    <DefineConstants>$(DefineConstants);IsNetFramework</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net472|AnyCPU'">
    <DocumentationFile>bin\Debug\net472\WeroSoft.Core.Library.xml</DocumentationFile>
    <DefineConstants>$(DefineConstants);Generation2</DefineConstants>
    <DefineConstants>$(DefineConstants);IsNetFramework</DefineConstants>
  </PropertyGroup>
  <!-- Define switches for build control *** RELEASE *** -->
  <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0|AnyCPU'">
    <DocumentationFile>bin\Release\net8.0\WeroSoft.Core.Library.xml</DocumentationFile>
    <DefineConstants>$(DefineConstants);Generation3</DefineConstants>
    <DefineConstants>$(DefineConstants);IsNetCore</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net48|AnyCPU'">
    <DocumentationFile>bin\Release\net48\WeroSoft.Core.Library.xml</DocumentationFile>
    <DefineConstants>$(DefineConstants);Generation3</DefineConstants>
    <DefineConstants>$(DefineConstants);IsNetFramework</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net472|AnyCPU'">
    <DocumentationFile>bin\Release\net472\WeroSoft.Core.Library.xml</DocumentationFile>
    <DefineConstants>$(DefineConstants);Generation2</DefineConstants>
    <DefineConstants>$(DefineConstants);IsNetFramework</DefineConstants>
  </PropertyGroup>

Ferner zu berücksichtigen

Neben den vorgestellten Details für die Beeinflussung und Steuerung des Compilerverhaltens in Code und Projektdatei. Müssen auch folgende Punkte bedacht werden:

  • Einbindung von NUGET-Paketen
  • Herstellung eigner NUGET-Pakete
  • Teil-Synchronisation des Build-Prozesses
  • Wiedererkennbarkeit einer Assembly

Einbindung von NUGET-Paketen

Die Einbindung von Nuget-Paketen, heute eine nicht wegzudenkende Teilfunktionalität des Herstellungsprozesses, muss ebenfalls spezifisch zur Zielplattform vorgenommen werden. Dazu eignet sich die Zentralisierung der Einbindung, sofern das Vorhaben mehrere Projekte aufweist. Die Schwelle der Menge empfehle ich tief anzusetzen (ca. 5 Projekte), denn die “Versionsschlacht” mit NUGET kann schnell unübersichtlich werden kann.

Untenstehend ist ein nicht vollständiges Beispiel einer zentralen NUGET-Definition für alle unsere 400 Projekte. Die Daten sind in der Datei Directory.Packages.props im Wurzelverzeichnis des eigenen Projektbereichs unterzubringen. Die Datei muss im Dateisystem hierarchisch oberhalb aller Projekte liegen. So findet MS-Build oder Visual Studio die Datei automatisch.

<Project>
	<PropertyGroup>
		<!-- Enable central package management -->
		<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>

		<!-- Define our currently supported frameworks -->
		<TargetFrameworks>net8.0-windows;net8.0;net48;net472</TargetFrameworks>
	</PropertyGroup>

	<!-- Define the Generation Flag for including NUGET packages -->
	<PropertyGroup Condition="$(TargetFramework) == 'net8.0' or $(TargetFramework) == 'net8.0-windows' or $(TargetFramework) == 'net48'">
		<WeroSoftGeneration>Generation_3</WeroSoftGeneration>
	</PropertyGroup>
	<PropertyGroup Condition="$(TargetFramework) == 'net472'">
		<WeroSoftGeneration>Generation_2</WeroSoftGeneration>
	</PropertyGroup>

	<!-- Define some global NUGET-packages. Note that this global declaration 
		     avoids overwriting the loaded NUGET by transitive usage. The result
			 of that is, that eventually a version routing must be established
			 in the project.                                                             -->

	<!-- ******************************************************************************* -->
	<!-- Generation specific packages  -->
	<!-- ******************************************************************************* -->
	<ItemGroup Condition="$(WeroSoftGeneration) == 'Generation_3'">
		
		<!-- Common packages -->
		<GlobalPackageReference Include="Newtonsoft.Json" Version="13.0.3" />

		<!-- Standard NuGet Feed - Standard packages -->
		<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
		<PackageVersion Include="System.ComponentModel.Composition" Version="8.0.0" />
		<PackageVersion Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
		<PackageVersion Include="System.Data.DataSetExtensions" Version="4.5.0" />
		<PackageVersion Include="System.Data.SqlClient" Version="4.9.0" />
	...
	</ItemGroup>

	<!-- Version depended packages -->
	<!-- ******************************************************************************* -->
	<!-- net8.0 and net8.0-windows packages  -->
	<!-- ******************************************************************************* -->
	<ItemGroup Condition="$(TargetFramework) == 'net8.0' or $(TargetFramework) == 'net8.0-windows'">
		<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
		<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
		<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8" />
	...
	</ItemGroup>

	<!-- ******************************************************************************* -->
	<!-- net48 packages  -->
	<!-- ******************************************************************************* -->
	<ItemGroup Condition="$(TargetFramework) == 'net48'">
		<PackageVersion Include="EntityFramework" Version="6.5.1" />
	...
	</ItemGroup>

	<!-- ******************************************************************************* -->
	<!-- net472 packages  -->
	<!-- ******************************************************************************* -->
	<ItemGroup Condition="$(WeroSoftGeneration) == 'Generation_2'">
		
		<PackageVersion Include="EntityFramework" Version="6.4.4" />

		<!-- Standard NuGet Feed - Test packages -->
		<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
		<PackageVersion Include="MSTest.TestAdapter" Version="3.6.4" />
		<PackageVersion Include="MSTest.TestFramework" Version="3.6.4" />
...
		
	</ItemGroup>

</Project>

Beachte, dass die Datei Directory.Packages.props die Pakete mit der Version des Paketes definiert, währenddem später in den einzelnen Projektdateien nur noch die Referenz auf die Paketversion eingebracht wird, was im untenstehenden Beispielausschnitt aus einer Projektdatei sichtbar ist.

  <ItemGroup Label="PackageReferencesNet" Condition="'$(TargetFramework)|$(Platform)'=='net8.0-windows|AnyCPU'">
    <PackageReference Include="EntityFramework" />
    <PackageReference Include="System.ComponentModel.Annotations" />
    <PackageReference Include="System.IO.Compression" />
    <PackageReference Include="System.Management" />
    <PackageReference Include="System.Runtime.Caching" />
  </ItemGroup>

Erstellung von NUGET-Paketen

Ein weiterer Aspekt von der Verwendung von NUGET ist die Erstellung einer eigenen NUEGT-Datei, die für die Weitergabe des Codes verwendet werden kann. Auch diese Datei kann so aufgebaut werden, dass mehrere Zielplattformen gleichzeitig unterstützt werden.

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
	<metadata minClientVersion="4.0">
		<id>WeroSoft.Core.Full</id>
		<version>3.0.0.0</version>
		<title>weroSoft Core Framework</title>
		<authors>weroSoft AG</authors>
...		
		<dependencies>
			<!-- ******************************************************************************************* 
			     .NET 8.0
			     ******************************************************************************************* -->
			<group targetFramework="net8.0">
				<!-- Standard NUGET Feed - Microsoft Packages -->
				<dependency id="EntityFramework" version="6.4.4" />
				<dependency id="Microsoft.CSharp" version="4.7.0" />
				<dependency id="System.ComponentModel.Annotations" version="5.0.0" />
...
			</group>
			
			<!-- ******************************************************************************************* 
			     .NET Framework 4.8
			     ******************************************************************************************* -->
			<group targetFramework=".NETFramework4.8">
				<!-- Standard NUGET Feed - Microsoft Packages -->
				<dependency id="EntityFramework" version="6.4.4" />
				<dependency id="Microsoft.CSharp" version="4.7.0" />
...
			</group>
		</dependencies>
	</metadata>
	<files>

		<!-- ******************************************************************************************* 
	         .NET 8.0
             ******************************************************************************************* -->
		<!-- Platform supporting libraries and dynamic libraries -->
		<file src="..\..\..\..\_BuildResults\core\WeroSoft.PlatformInvoke.Library.dll" target="lib\net8.0\" />
		<file src="..\..\..\..\_BuildResults\core\WeroSoft.PlatformInvoke.Library.xml" target="lib\net8.0\" />
		<file src="..\..\..\..\_BuildResults\core\**\WeroSoft.PlatformInvoke.Library.resources.dll" target="lib\net8.0\" />
...		

		<!-- ******************************************************************************************* 
	         .NET Framework 4.8
             ******************************************************************************************* -->
		<!-- Platform supporting libraries and dynamic libraries -->
		<file src="..\..\..\..\_BuildResults\net\WeroSoft.PlatformInvoke.Library.dll" target="lib\net48\" />
		<file src="..\..\..\..\_BuildResults\net\WeroSoft.PlatformInvoke.Library.xml" target="lib\net48\" />
		<file src="..\..\..\..\_BuildResults\net\**\WeroSoft.PlatformInvoke.Library.resources.dll" target="lib\net48\" />
...
	</files>
</package>

Pre/Post-Build-Step Synchronisation

Aufgrund des Umstandes, dass bei einer Multi-Target Projektdefinition MS-Build die jeweiligen Ziele parallel herstellt (Standardeinstellung), kann es bei den verwendeten Pre- und Post-Build Verarbeitungen zu einem Durcheinander kommen. Wir haben für diesen Zweck früher konventionelle Batchdateien verwendet. Innerhalb von diesen haben wir Ausgaben vorgenommen, was mit dem Einsatz der Multi-Build-Strategie zu einem Durcheinander im Ausgabefenster von Visual Studio führte.

Wir haben die Batch-Dateien durch PowerShell-Scripts abgelöst. Das hat ermöglich, dass wir eine Synchronisation einbauen konnten und nun die Ausgaben geordnet nacheinander im Ausgabefenster haben und so viel Zeit in der Analyse bei Fehlern eingespart werden kann, weil klar ist was, während dem Herstellen des Projekts effektiv abgelaufen ist.

# ----------------------------------------------------------------------------------------
# Visual Studio project post build (PowerShell-Version)
# ----------------------------------------------------------------------------------------

param (
    [string]$ProjectDir,
    [string]$ProjectName,
    [string]$OutputDirectory,
    [string]$TargetName,
    [string]$ConfigurationName,
    [string]$CopyAssemblies
)

# Mutex to ensure only one post-build job runs at a time
$mutexName = "Global\PostBuildMutex"
$mutex = New-Object System.Threading.Mutex($false, $mutexName)

# Wait 5 seconds for mutex acquisition (must be defined in milliseconds)
$mutex.WaitOne(5000)

try {

    # Build own variables
    $TechnologyName = ""

    if ($OutputDirectory -match "net8.0") {
        $TechnologyName = "core\"
    } elseif ($OutputDirectory -match "net48") {
        $TechnologyName = "net\"
    } else {
        $TechnologyName = "netG2\"
    }

...
  
    # Write some output information to help debugging
    Write-Host "**********************************************************************************"
    Write-Host "**      weroSoft post build worker for technology - $TechnologyName"
    Write-Host "**********************************************************************************"
    Write-Host "ProjectDir = $ProjectDir"
    Write-Host "ProjectName = $ProjectName"
    Write-Host "OutputDirectory = $OutputDirectory"
    Write-Host "TargetName = $TargetName"
    Write-Host "Technology = $TechnologyName"
    Write-Host "SourcePath = $SourcePath"
    Write-Host "TargetPath = $TargetPath"
    Write-Host "ConfigurationName = $ConfigurationName"
    Write-Host "CopyAssemblies = $CopyAssemblies"
    Write-Host "CurrentPath = $(Get-Location)"

...

    Write-Host "Post-build script completed successfully."
} finally {
    # Release the mutex
    $mutex.ReleaseMutex()
    Write-Host "Mutex released."
}

Wiedererkennbarkeit einer Assembly

Da mit dem Einsatz der Multi-Target-Technik unsere Assembly jeweils in drei Versionen entstehen, kann es in der Praxis bei der Handhabung zu Verwechslungen führen, da alle drei Versionen den gleichen Namen haben und Mischungen von Versionen sich negativ auswirken können. Um eine Assembly im Zweifelsfall ohne Einsatz von Entwicklertools zu einer Version zuordnen zu können kennzeichnen wir die Assembly mit Produktversion, die im File-Explorer in den Eigenschaften der Date abgefragt werden kann. Dabei kommen folgende Kennzeichnungen vor:

Zielplattform - Assembly Informational Version

.NET Core
3.0 Release, Generation 3, .NET
.NET Framework 4.8
3.0 Release, Generation 3, .NET Framework
.NET Framework 4.7.2
3.0 Release, Generation 2, .NET Framework

Fazit

Mit dem Einführung von Multi-Targeting im Build-Prozess sind wir nun gerüstet für die Herstellung unterschiedlicher Versionen des gesamten Frameworks. Damit können wir die Kunden schlussendlich in einer neuen Dimension mit unserem Framework unterstützen und die Migration der Kundenvorhaben elegant in die technologische Zukunft begleiten.