hero-bp-migrating-from-net-framework-to-net-core-v2

Im letzten Jahr hat Microsoft angekündigt, dass .NET Framework 4.8 die letzte größere Version des vollständigen .NET Framework sein würde. Da .NET Core bereits weitgehend für die plattformübergreifende Entwicklung verwendet wurde, war für uns die Zeit für die Migration von Framework auf Core gekommen.

.NET Core ist nicht nur plattformübergreifend, sondern bietet auch eine bessere Leistung. Zudem verfügt es über mehrere Verbesserungen gegenüber dem .NET Framework und wird uns viele Jahre begleiten. Nicht zuletzt ist sie Open-Source – und nicht proprietär wie Framework.

Meme einer älteren Dame bei der .NET-Migration
Erstellt mit Meme Generator

Nach .NET Core 3.x wird die nächste größere Version .NET 5 sein. (Wir überspringen „Version 4“, da es nur eine kosmetische Änderung beinhalten wird. Und „Core“ wird der Einfachheit halber wegfallen, da es zu dem Zeitpunkt kein „Framework“ mehr geben wird.) Die .NET 5-Vollversion soll Ende 2020 erscheinen. Deshalb haben wir beschlossen, unsere Projekte entsprechend vorzubereiten.

Um all denjenigen zu helfen, die gerade das Gleiche durchmachen, möchten wir hier einige Erfahrungen und all das teilen, was bei uns gut funktioniert hat.

Empfehlungen zur Migration auf .NET Core

Wir haben den Prozess durchlaufen und können ihn in vier Hauptphasen unterteilen.

  • Codeanalyse: Finden Sie heraus, wie kompatibel Ihre Lösung mit dem .NET Core Framework ist.
  • Migrationstypen: Entscheiden Sie, wie Sie migrieren möchten. Ausschlaggebend ist die Art und Weise der Entwicklung und Veröffentlichung Ihrer Software. Verschaffen Sie sich einen Überblick darüber, wie jedes Projekt in die Migrationsstrategie passt, abhängig von seiner Komplexität und der Menge an Windows-spezifischem Code.
  • Migration der .csprojs-Dateien: Konvertieren Sie Ihre Projektdateien in das obligatorische Microsoft.NET.Sdk-Format.
  • Codemigration: Konvertieren Sie Ihren C#-Code in .NET Core/.NET Standard.

Viele Migrationsprozesse hängen nicht nur von Ihrer Lösung ab, sondern auch davon, wie Ihre Software entwickelt und veröffentlicht wird. Schauen wir uns das Ganze genauer an!

Code-Analyse

Betrachten Sie Ihre Lösung in .NET Framework. Der größte Teil der Codebasis ist plattformunabhängig und funktioniert auf jedem Betriebssystem. Einiges jedoch funktioniert nur unter Windows. Bevor Sie mit der Migration beginnen, sollten Sie wissen, welche Bibliotheken für Sie problematisch sind.

Microsoft hat ein Tool – den Portability Analyzer – entwickelt, das sich auf die Analyse Ihres Codes konzentriert und Ihnen einen ausführlichen Bericht über die Kompatibilität zwischen Ihrem aktuellen Framework und den ausgewählten Ziel-Frameworks liefert (etwa so wie in der Abbildung unten dargestellt).

Portability Analyzer Tool

Das Tool kann Ihnen zwar einen guten Ausgangspunkt bieten, aber vertrauen Sie den Ergebnissen nicht blind. Es gibt einige falsch-negative Ergebnisse, hauptsächlich in Bezug auf Drittanbieter-Bibliotheken. Validieren Sie daher immer mit anderen Tools, z. B. mit dem .NET API Catalog. Damit können Sie überprüfen, welche .NET-Bibliotheken verfügbar sind, und welche Nuget-Alternativen es für alle anderen gibt.

Arten der Migration

Die Migration von Lösungen mit mehr als 60 Projekten, von denen einige gemeinsam mit anderen Lösungen genutzt werden, kann schwierig sein. Wenn Sie sich für eine direkte Migration von Framework zu Core entscheiden, müssen Sie die Migration vollständig durchführen. Das heißt: Sobald Sie eines der Projekte migrieren, müssen Sie alle Projekte in .NET Core migrieren. Da die Codebasen von .NET Framework und .NET Core unterschiedlich sind, können Sie Framework-Projekte nicht in Core-Projekte importieren und umgekehrt.

Wenn Ihre Software derzeit nicht verteilt ist, haben Sie die große Chance, die Migration in einem einzigen Schritt durchzuführen. Wenn Sie Ihre Software jedoch stetig weiterentwickeln und neue Versionen herausbringen, ist eine solche Migration nicht ratsam.

Bei OutSystems und insbesondere bei unserer Software Service Studio müssen wir angesichts ständiger Neuentwicklungen Folgendes sicherstellen:

  • Unsere aktuelle Windows-Software funktioniert weiter und wird wie gewohnt veröffentlicht.
  • Teams entwickeln und ergänzen sowohl die aktuelle Lösung (nur Windows) als auch die neue Lösung (.NET Core plattformübergreifend) weiter.

In unserem Fall wäre es unmöglich, alles auf einmal zu migrieren, da dies unsere aktuelle Lösung beeinträchtigen würde. Eine Möglichkeit ist die gemeinsame API zwischen Framework und Core — wie im Bild unten dargestellt.

.net core und .net Framework-Beziehung
Link zum Bild: Wikipedia

Die Shared API (bekannt als .NET-Standard) ist eine Teilmenge der Framework- und Core-Plattformen und ermöglichte uns einen vorsichtigeren Ansatz bei der Migration. Dabei haben wir die folgenden Schritte ausgeführt:

1. Aufteilen der Projekte in zwei Kategorien: Projekte, die nur eine Feinabstimmung (einfache Migration) benötigen, und Projekte, die teilweise/vollständig überarbeitet werden müssen, z. B. Projekte mit grafischen Komponenten der Windows Presentation Foundation (WPF).

2. Für Projekte, die einfach zu migrieren sind:

  • Hier migrieren Sie von .NET Framework zu .NET Standard. Sie bilden die gemeinsame Codebasis zwischen der alten reinen Windows-Lösung und der neuen .NET-Core-Lösung, und beide Lösungen können sie nutzen. Schauen Sie sich ein Abhängigkeitsdiagramm der Lösung an und beginnen Sie mit den Projekten an der Basis der Pyramide, d. h. Projekten, die nicht von anderen abhängen.
  • Wenn eines dieser Projekte zu mehreren Lösungen gehört, müssen Sie möglicherweise die aktuelle Framework-Version und die .NET-Standardversion weiter entwickeln. Dabei haben Sie zwei Möglichkeiten: Multi-Target oder Conditional Building.
<!--Multi-target option-->
<PropertyGroup>
    <TargetFrameworks>net461;netstandard2.0</TargetFrameworks>    
</PropertyGroup>

<!--Conditional building option-->
<PropertyGroup>
    <TargetFramework Condition="'$(Configuration)' != 'CustomConfiguration'">net461</TargetFramework>
    <TargetFramework Condition="'$(Configuration)' == 'CustomConfiguration'">netstandard2.0</TargetFramework>
</PropertyGroup>

Das Endergebnis jeder Option ist gleich, aber es gibt einen wichtigen Unterschied. Multi-Target erstellt immer n Framework-Versionen, was die Kompilierung verlangsamt. Außerdem können gesperrte Dateien auftreten, da n Projekte gleichzeitig erstellt werden und auf die Ressourcen der Projekte zugreifen. Aus diesem Grund haben wir uns nach Möglichkeit für die Conditional-Building-Strategie entschieden.

Tipp: Sie können die Datei Directory.Build.props verwenden, um globale Eigenschaften für Ihre Projekte zu deklarieren, nämlich eine Conditional-Building-Option für alle .csprojs. Die Datei Directory.Build.props wirkt sich auf alle Projekte im Lösungsverzeichnis aus.

3. Projekte, die entweder zu viel Windows-spezifischen Code haben, z. B. WPF-Komponenten, oder umständlich zu migrierenden Code, wie z. B. SOAP Introspection, können Sie in .NET Framework beibehalten, und die Strategie für die Portierung später definieren. Alternativ können Sie sie auch streichen und auf der Grundlage anderer kompatibler Bibliotheken neu erstellen. Wenn Sie sich für eine Portierung entscheiden, müssen Sie neue .NET-Core-Projekte erstellen, in denen Sie mit der Implementierung ihrer geänderten Version beginnen.

4. Erstellen Sie eine neue .NET-Core-Lösung, bei der Sie nach und nach jedes migrierte Projekt importieren. Wenn das letzte Teil des Puzzles passt, haben Sie es geschafft und können Ihre gesamte Software in .NET Core nutzen.

Tipp: Wenn Sie die mehrstufige Strategie verfolgen, sollten Sie auch Ihre Projekte, die in .NET Framework verbleiben, auf Version 4.7.2 aktualisieren. Auch wenn Sie .NET-Standard-Projekte neben Framework-4.6.1-Projekten haben können, ist es unserer Erfahrung nach besser, ein Upgrade durchzuführen. Dies ist in den meisten Fällen ein einfacher Schritt, der Ihnen viel Zeit und Probleme mit Framework-Inkompatibilitäten sparen kann. Microsoft empfiehlt ihn ebenfalls.

Migrieren der .csproj-Dateien

Wenn Sie ein Projekt auf .NET Standard/.NET Core migrieren, müssen Sie das csproj-Format „Microsoft.NET.Sdk“ übernehmen. Das Ändern Ihrer .csproj-Datei in SDK-Stil kann je nach den Besonderheiten Ihres Projekts umständlich sein.

Hier sind die Hauptunterschiede (und einige Probleme), denen wir begegnet sind:

  • Dateien innerhalb des Projekts werden automatisch zur @(Compile)-Liste hinzugefügt. Die langen Listen von <Compile Include=”..>-Elementen sind nicht mehr nötig.
  • Wenn Sie während der Kompilierung Dateien erzeugen und diese in Ihr Projekt einbinden möchten, kommt es zu Problemen. Zunächst müssen Sie die Dateien explizit einbinden, wie Sie es früher getan haben. Beim zweiten oder jedem weiteren Build des Projekts werden Sie jedoch Fehler im Zusammenhang mit Duplikaten in der @(Compile)-Liste erhalten. Die Gründe dafür:

1. Dateien, die während des ersten Builds kompiliert und generiert wurden, werden aufgrund des SDK-Typs csproj automatisch zur @(Compile)-Liste hinzugefügt.

2. Dateien, die während der Kompilierung vom n-ten Build generiert wurden, werden der @(Compile)-Liste hinzugefügt. Dies geschieht durch den Code in Ihrer .csproj-Datei, wo sie sie explizit einschließen.

Damit dies funktioniert, schließen Sie die generierten Dateien nur ein, wenn sie noch nicht in der Liste enthalten sind:

<Compile Include="@(GeneratedFiles)" Exclude="@(Compile)" />

Es kann jedoch erneut zu einem Problem kommen. Die enthaltenen Dateien können ihre vollständigen Pfade haben, während Dateien von @(Compile) nur relative Pfade haben. Letztendlich haben wir für diese Fälle die folgende Lösung genutzt:

<Target Name="IncludeGeneratedFiles">
    <ItemGroup>
        <CompileList Include="@(Compile->'%(FullPath)')" />
    </ItemGroup>
    <ItemGroup>
       <CompileList Include="@(GeneratedFiles)" Exclude="@(CompileList)" />
    </ItemGroup>
    <ItemGroup>
        <Compile Remove="@(Compile)" />
        <Compile Include="@(CompileList)" />
    </ItemGroup>
  </Target>

Beachten Sie auch: Wenn Sie zur Kompilierzeit Typescript-Dateien generieren, die in die DLL eingebettet werden müssen, müssen Sie dies zum EmbeddedResource-Tag hinzufügen:

<EmbeddedResource Include="FileGenerated.ts" Type="Non-Resx" WithCulture="false" />

 

  • Assembly-Infodateien werden jetzt automatisch generiert. Wenn Sie bereits über Assembly-Infodateien verfügen und diese behalten möchten, fügen Sie zur PropertyGroup einfach <GenerateAssemblyInfo>false</GenerateAssemblyInfo> hinzu.
  • Standardmäßig wird dem Ausgabepfad nun der Name des Ziel-Frameworks angehängt. Um dies zu deaktivieren, nehmen Sie die folgende Eigenschaft:
<PropertyGroup> 
    <AppendTargetFrameworkToOutputPath>
        false
    </AppendTargetFrameworkToOutputPath>
</PropertyGroup> 

 

  • Da der SDK-Stil implizit Sdk.targets am Ende der Datei importiert, werden vordefinierte MSBuild-Ziele wie <BuildDependsOn> oder <CompileDependsOn> am Anfang der csproj überschrieben und funktionieren nicht. Sie können den Import explizit hinzufügen und Ihre Erweiterung nachträglich einfügen. Oder Sie ändern die Art und Weise, wie die Ziele definiert werden:
<!--Original. Won't work-->
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        
        <BuildDependsOn>
            CustomTarget;
            $(BuildDependsOn)
        </BuildDependsOn>
    </PropertyGroup>
    <Target Name="CustomTarget">
        <!--Stuff-->
    </Target>
</Project>
<!--Solution one: add import explicitly -->
<Project>
    <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
    <PropertyGroup>        <TargetFramework>netstandard2.0</TargetFramework>
    </PropertyGroup>
    <Target Name="CustomTarget">
        <!--Stuff-->
    </Target>
    <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
    <PropertyGroup>
        <BuildDependsOn>
            CustomTarget;
            $(BuildDependsOn)
        </BuildDependsOn>
    </PropertyGroup>
</Project>
<!--Solution two: adapt target. We recommend this approach-->
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>  <TargetFramework>netstandard2.0</TargetFramework>
    </PropertyGroup>
    <Target Name="CustomTarget" AfterTargets="Build">
        <!--Stuff-->
    </Target>
</Project>

 

  • Die Verwaltung von Nuget-Paketen, die in Nuget 2 über package.config durchgeführt wurde, wird jetzt auf sauberere Weise über PackageReference erreicht:
 <ItemGroup>  
      <PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.5.0" />
      <PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.3.0" />
  </ItemGroup>

 

  • Viel unnötiger Code im SDK-Format. Auch wenn er einer korrekten Erstellung des Projekts meist nicht im Wege steht, behindert er die Lesbarkeit und Wartung Ihrer .csprojs-Datei. Wir empfehlen Ihnen, allen unnötigen Code zu entfernen. Ein Beispiel für Code, der leicht weggelassen werden kann:
<!--Old non-SDK format-->
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--New SDK-format, with unnecessary code-->
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--New SDK-format, clean way-->
<Project Sdk="Microsoft.NET.Sdk">

Um .csprojs in das SDK-Format zu konvertieren, gibt es ein einfach zu bedienendes Tool, das Sie bei dieser Aufgabe gut unterstützen kann: CsProjToVS2017.

Migrieren des Codes

Für die Migration des Codes selbst gibt es nicht viel allgemeingültigen Rat, da jedes Projekt seine Spezifikationen und seinen eigenen Anteil an nicht kompatiblem Code hat. An diesem Punkt sollten Sie eine gute Vorstellung von den problematischen Drittanbieter-Bibliotheken haben, die nicht plattformübergreifend sind. Sie werden sich nach entsprechenden Alternativen umschauen.

Wenn Sie auf .NET Standard migrieren, nach neuen Bibliotheken suchen und in .NET Standard keinen Ersatz finden, heißt das nicht, dass es keine geeignete Bibliothek gibt.

Wir haben einen kleinen Test durchgeführt, um zu bestätigen, dass die Klasse System.Diagnostics.Activity kein Problem darstellt. Tatsächlich existiert dieser Namespace nicht in .NET Standard 2.0, wohl aber in .NET Core und in einem Nuget-Paket über die Klasse System.Diagnostics.DiagnosticSource. Der Test bestand aus der Generierung einer .NET Standard 2.0 Bibliothek unter Verwendung des genannten Nuget-Pakets und dem späteren Import dieser Bibliothek in eine .NET Core Konsolen-Applikation. Die Ergebnisse waren identisch, wenn die Konsolen-App unter Windows und Mac ausgeführt wurde.

Wenn eine Bibliothek nicht in .NET Standard existiert, sollten Sie sich zuerst fragen: „Existiert sie in .NET Core und über Nuget?“. Erst, wenn die Antwort Nein ist, müssen Sie nach Alternativen für nicht existierende Bibliotheken suchen. Denken Sie daran, dass .NET Standard nur ein Zwischenschritt ist und das Endziel darin besteht, Ihre Software auf .NET Core zu migrieren.

Es wird auch vorkommen, dass Sie keine Bibliothek finden, die für alle Betriebssysteme funktioniert, und dass Sie Ihren Code nicht von einigen betriebssystemabhängigen Bedingungen befreien können. Ein Beispiel dafür ist der Code zur Verwaltung der Berechtigungen von Dateien/Ordnern. Zum jetzigen Zeitpunkt gibt es in .NET keine plattformübergreifende Bibliothek, so dass Sie etwas in der Art benötigen:

private static void SetPermissionsToEveryone(string directory) {
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
        SetPermissionsToEveryoneWindows(directory);
    } else {
        SetPermissionsToEveryoneUnix(directory);
    }
}

private static void SetPermissionsToEveryoneUnix(string directory) {
    var unixFileInfo = new UnixFileInfo(directory) {
        // set file permission to 777
        FileAccessPermissions =
            FileAccessPermissions.UserReadWriteExecute
            | FileAccessPermissions.GroupReadWriteExecute
            | FileAccessPermissions.OtherReadWriteExecute
        };
}

In Zukunft wird .NET Core wahrscheinlich eine Abstraktion bereitstellen, die dies im Hintergrund erledigt. Bis dahin haben wir das Problem wie beschrieben gelöst.

Abschließend gibt es ein Paket, das Sie in Ihrem Projekt installieren können und das Ihnen während des Programmierens mitteilt, ob eine bestimmte Methode für alle Ziel-Frameworks, die Ihr Projekt ausgewählt hat, existiert. Dies könnte sich als nützlich erweisen.

Zusammenfassung

Selbst wenn Sie alles richtig gemacht haben und der Code kompiliert wird, kann der Prozess zur Laufzeit noch scheitern. Dies wird wahrscheinlich durch einen von zwei Gründen verursacht: Entweder ist eine Methode aus einer Ihrer Nuget-Referenzen nicht für das Betriebssystem implementiert, auf dem Sie arbeiten (sogar .NET Core Nuget-Referenzen haben unfertige, d. h. nicht implementierte Methoden) oder die Implementierung für das vorliegende Betriebssystem ist fehlerhaft, während sie für das andere stabil ist.

Deshalb sollten Sie sicherstellen, dass Sie Ihre Tests gegen die migrierte Codebasis auf allen Betriebssystemen durchführen, auf denen Ihre Applikation verteilt werden soll. Erwarten Sie nicht, dass eine stabile Version Ihrer Applikation auf macOS läuft, nur weil alle Windows-Tests erfolgreich waren.

So hat das Ganze bei uns funktioniert. Es war nicht immer einfach und es hat einige Zeit gedauert, aber schließlich haben wir es geschafft. Wir hoffen, Sie kommen ebenfalls gut zum Ziel!