.Net 5 App Trimming am Beispiel MessageCommunicator

Das Tool MessageCommunicator ist so konzipiert, dass es möglichst portabel sein soll. Idealerweise als schlichte Exe-Datei (bzw. ausführbare Datei auf Linux oder macOS). Mit dem Parameter PublishSingleFile ist das seit .Net Core 3.0 grundsätzlich möglich, einziges Problem ist lediglich die Größe der ausführbaren Datei. Man kann es sich aussuchen: Entweder man verteilt eine kleine Datei, setzt aber dann ein installiertes .Net am Zielsystem voraus, oder man hat eine größere Datei und bringt die Abhängigkeiten aus .Net direkt mit. Ich persönlich bevorzuge letzteres – es ist schlicht für alle Beteiligten einfacher, wenn eine Applikation möglichst wenig Voraussetzungen an das Zielsystem stellt. Mit den neuesten Funktionen rund um das App Trimming [1] kann an der Größe der zu verteilenden Dateien schrauben. In diesem Artikel möchte ich dazu meine Erfahrungen aus dem Projekt MessageCommunicator weitergeben.

App Trimming Allgemein

Aktuell verwende ich nachfolgenden Befehl um die Gui des MessageCommunicator Projekts für ein Release zu packen. Wichtig dabei sind folgende Parameter:

  • -p:PublishSingleFile=true
  • -p:IncludeNativeLibrariesForSelfExtract=true
  • -p:PublishTrimmed=true
  • -p:TrimMode=Link
dotnet publish -c Release -f net5.0 --self-contained true 
  -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true 
  -p:PublishTrimmed=true -p:TrimMode=Link --runtime win-x86 -o 
  "./publish/MessageCommunicator (Win X86)" ./MessageCommunicator.TestGui

Zunächst einmal ein paar Worte zu den ersten Beiden. PublishSingleFile gab es wie oben erwähnt schon seit .Net Core 3.0 und sorgt dafür, dass die eigene Applikation und alle Abhängigkeiten in eine einzelne ausführbare Datei gepackt werden. IncludeNativeLibrariesForSelfExtract ist erst später mit .Net 5 dazugekommen [2]. Standardmäßig packt .Net 5 nicht mehr alles in eine ausführbare Datei, sondern lässt einige wenige separat im Ordner. Mit dem Parameter IncludeNativeLibrariesForSelfExtract kann man .Net aber dazu zwingen, sich genauso wie vorher zu verhalten. Etwas interessanter für diesen Artikel sind allerdings die nachfolgenden Parameter PublishTrimmed und TrimMode.

PublishTrimmed existiert ebenfalls seit .Net Core 3.0 und sorgt dafür, dass nur die Assemblies mit ausgeliefert (bzw. verpackt) werden, die durch die eigene Applikation auch tatsächlich benötigt werden. Für genau dieses Verhalten steht auch der zugehörige Parameter TrimMode, dieser wäre standardmäßig auf CopyUsed gesetzt. Im gezeigten Beispiel oben verwende ich aber TrimMode=Link und genau das ist die Neuerung in .Net 5. Hiermit wird versucht, mehr im Detail nachzuschauen, was alles weggelassen werden kann (Klassen, Methoden, etc.). Klingt zunächst gut, hat aber vor allem bei der Verwendung von Reflection seine Fallstricke. Der unter [1] verwiesene Artikel nennt dazu schon sehr viele Details, daher möchte ich im Folgenden eher auf meine Erfahrungen bei meinem Projekt MessageCommunicator eingehen.

Erfahrungen im Projekt MessageCommunicator

Zunächst einmal zu den Problemen. Trimming auf Assembly-Ebene (TrimMode=CopyUsed) hat ohne Probleme funktioniert. Das SDK hat scheinbar alle Abhängigkeiten gut erkannt. Nach dem Umstellen des TrimMode auf Link sah es zunächst auch gut aus, die normale Oberfläche, starten von Verbindungen, senden und empfangen von Nachrichten hat alles gut funktioniert. ABER: Bei der Konfigurationsoberfläche zu einem Profil war wie im folgenden Screenshot zu sehen praktisch nichts mehr sichtbar. Tatsächlich müssten beispielsweise in der Gruppe ‚Connection‘ drei Parameter sichtbar sein.

Im Hintergrund wird ein PropertyGrid verwendet, welches für alle relevanten Eigenschaften des gebundenen Objekts zugehörige Dialogfelder generiert. Nun könnte man meinen, das Problem ist völlig klar. Auf die hier notwendigen Eigenschaften wird außer per Reflection nie irgendwo im Programmcode zugegriffen, somit müssten diese Eigenschaften wegen dem Trimming rausgeflogen sein. Nach längerem hin und her probieren z. B. mit dem DynamicDependencyAttribute [3] kam ich zu keinem Ergebnis. Zurück zur Analyse des eigentlichen Fehlers bin ich auf eine nachfolgende Exception gestoßen, die beim Aufbau des PropertyGrids geschmissen wird.

Hier beschwert sich .Net, dass es das ErrorsChanged Event in der Klasse ConfigurablePropertyMetadata nicht findet. Das etwas Kuriose dabei ist, dass es sich lediglich um eine Implementierung des Interfaces INotifyDataErrorInfo handelt – also an sich tiefster Standard. Die Lösung des Problems war aber tatsächlich nachfolgendes Coding, was ich der Übersichtlichkeit halber direkt in der Program.cs vor der Main Methode eingefügt habe.

// Dependencies for .Net 5 App Trimming
//  INotifyDataErrorInfo -> Without that we get binding errors in the PropertyGrid
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(INotifyDataErrorInfo))]
public static void Main(string[] args)
{
    ...

Über das Attribute DynamicDependencyAttribute kann man hier angeben, dass man alle Member des Typs INotifyDataErrorInfo benötigt. Das wars, ansonsten keine Probleme mit dem neuen Trimming-Modus.

Wie viel kleiner wird die Applikation tatsächlich?

Nachdem alles grundsätzlich funktioniert kommt die tatsächlich interessante Frage: Wie viel bringt es? Um diese Frage zu beantworten habe ich drei verschiedene Varianten miteinander verglichen. Je Variante einmal die gepackte ausführbare Datei direkt wie sie ist (Raw) und einmal gezippt (per WinRAR, Kompressionsmethode ’normal‘).

No TrimmingTrimming (CopyUsed)Trimming (Link)
Rawca. 80,7 MBca. 53,7 MBca. 42,9 MB
Gezipptca. 34,4 MBca. 22,2 MBca. 17,5 MB

Vergleicht man von links nach rechts, so erhalte ich hier bei Trimming (Link) in etwa die Hälfte zu No Trimming. Der tatsächliche Benefit durch den TrimMode=Link ist zwar nicht mehr so groß, braucht sich aber dennoch nicht verstecken. Durch das Zippen der Datei lässt sich schließlich noch einiges herausholen und hat lediglich den Nachteil, dass der Anwender am Zielsystem die Datei noch entpacken muss. In der letzten Variante (gezippt + TrimMode=Link) bedeutet das, man erhält ein kompiliertes .Net Gui-Programm mit sämtlichen Abhängigkeiten in einem 17,5 MB kleinen Download!

Fazit

Insgesamt ist das Trimming schon eine feine Sache, insbesondere der Modus CopyUsed. Bei dem neuen Modus Link sollte man etwas Acht geben. Zwar kann man hier auch noch etwas rausholen, allerdings sollte von einem höheren Aufwand beim Testen und bei der Anpassung des Codes ausgegangen werden – Selbst bei dem kleinen Beispiel hier hat es etwas gedauert, bis alles lief. Für mich stellt sich derzeit auch die Frage, ob es mit diesem Modus nicht auch besser wäre, die Unittests direkt mit der gepackten Variante auszuführen. Ansonsten kommt man nicht vorbei, alle Funktionen in der gepackten Variante manuell durchzutesten.

MessageCommunicator selbst werde ich in den nächsten Releases aber dennoch mit dem neuen TrimMode=Link veröffentlichen. Hier ist es allgemein nicht aufwändig, vor einem Release nochmals alle Funktionen händisch durchzutesten. Davon unabhängig ist MessageCommunicator ein schönes kleines Projekt, um eben dieses Feature von .Net im praktischen Einsatz zu testen.

Verweise
[1] = App Trimming in .NET 5 | .NET Blog (microsoft.com)
[2] = Announcing .NET 5.0 | .NET Blog (microsoft.com)
[3] = DynamicDependencyAttribute Class (System.Diagnostics.CodeAnalysis) | Microsoft Docs

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.