Laden...

NetCore 1.1 optimiert eine (für Reflection) benötigte Referenz weg - wie umgehe ich das?

Erstellt von Taipi88 vor 6 Jahren Letzter Beitrag vor 6 Jahren 2.121 Views
Taipi88 Themenstarter:in
1.029 Beiträge seit 2010
vor 6 Jahren
NetCore 1.1 optimiert eine (für Reflection) benötigte Referenz weg - wie umgehe ich das?

Hi,

ich hab das Problem, dass ich für eine aktuelle ASP.NET Anwendung im Konfigurationsbereich Reflection einsetzen möchte.

Das ganze würde auch eigentlich funktionieren - wenn NetCore nicht verschiedene Referenzen wegoptimieren würde.

Beispiel:

Hauptassembly: AuthHost
verweist auf Dapper.Mssql

Dapper.Mssql verweist auf Dapper

Beide Verweise werden auch als Referenzen im Visual Studio angezeigt - rufe ich jedoch:


var referencedAssemblies = Assembly.GetEntryAssembly().GetRefenrencedAssemblies();

auf, taucht die Refenrenz Dapper in der Variable "referencedAssemblies" nicht auf, bzw. nur dann auf, wenn ich im Code explizit einen Typ aus der Assembly Dapper verwende, was ich allerdings nicht möchte, da ich ja im Bereich der Startkonfiguration mit Reflection arbeiten möchte.

Mein Anliegen?
Ist es möglich dem Compiler zu sagen, dass er diesen Verweis doch bitte "stehen" lassen soll? Wenn ja: Wie geht das?

LG

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo Taipi88,

wenn ich im Code explizit einen Typ aus der Assembly Dapper verwende

War das nicht schon immer so (zumindest ab .net 2.0)?

Du musst rekursiv selber weiter schreiten um so die Abhängigkeiten auflösen zu können.
Also in referencedAssemblies wieder die referenzierten laden, usw.

Ich bin froh dass der Compiler nur das ausgibt was er auch benötigt. Sonst wäre das mit den Meta-Packages auch eine grobe Sache 😉

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

Taipi88 Themenstarter:in
1.029 Beiträge seit 2010
vor 6 Jahren

Hi gfoidl,

sry - da war ich unvollständig. Auch die Hauptassembly hat einen direkten Verweis in VS auf diese "Dapper"-Assembly.

Das Problem ist vielmehr, dass das Hauptprojekt nur über Reflection auf die Assembly zugreift, womit die Referenz eben wegoptimiert wird. (Was ich ja normal gut finde - nur eben für diese Assembly nicht^^)

Ich habe mir jetzt geholfen, indem ich die Klasse, die ich dynamisch instanziieren möchte noch einmal abgeleitet habe und damit auch einen direkten Zugriff auf die Assembly mache, womit die Referenz nicht länger wegoptimiert wird.

Die Frage ist nur die, ob man dem Compiler sagen kann, dass man eine bestimmte Referenz gerne behalten möchte...

LG

Edit:

Für den Background mal die "eigentliche" Projektstruktur:

  1. Firmenname.AuthHost (AspnetCore Projekt)
  2. Firmenname.Data (Assembly mit wichtigen Interfaces)
  3. Firmenname.Data.Dapper (Assembly mit Standardimplementierungen auf DapperBasis)
  4. Firmenname.Data.Dapper.Mssql (Erweiterung zu Data.Dapper, die nur eine Klasse enthält die ich per Reflection anspreche - aber eben nicht in Data.Dapper gehört wegen direktem Verweis auf Mssql)

Und eben Eintrag Nummer 4 ist direkt referenziert - wird aber denn vom Compiler wegoptimiert...

LG

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo Taipi88,

zur Frage ob der Compiler dafür einen Switch hat, kann ich nichts sagen bzw. mir ist keiner bekannt und nachgeschaut hab ich auch nicht.

Ganz anders gedacht: ist es möglich dass diese Infos schon zur Entwurfszeit generiert werden? Z.B. per eigenen Build-Task. Da lässt sich das einfacher ermitteln.

nur über Reflection auf die Assembly zugreift

Das ist ja klar. Warum soll er diese Referenz dann auch halten?

Wie greifst du mit Refletion auf die Assembly zu?
Wenn du auf einen exportierten Typen zugreifst typeof so bleibt die Referenz auch erhalten.

Wenn die Assembly bzw. ein exportierter Type per Type.GetType o.ä. geladen wird, so ist folgendes möglich: Package Microsoft.Extensions.DependencyModel hinzufügen und dann DependencyContext.Default.GetDefaultAssemblyNames(). Ich hab das auch erst eben gefunden, schau mal ob das für deinen Anwendungsfall auch passt.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

16.807 Beiträge seit 2008
vor 6 Jahren

Meine Meinung ganz ehrlich: ich hoffe, das geht nicht.

Du untergräbst mit dieser Art und Weise jegliche Konzeption, wie Konfigurationen in moderner Software weitergegeben und injiziert wird.
Wo siehst Du hier die Vorteile? Ich sehe nur Nachteile, davon abgesehen, dass das Konfigurationsmodell in ASP.NET so gar nicht gedacht ist und in einigen Umgebungen so auch überhaupt nicht wartbar ist (zB. IIS, Docker, Azure, AWS, Tokenization während Build/Release (CI)).

PS: auch Deine Namespace-Aufteilung wäre vermutlich rein, wenn man die Guidelines beachtet, so korrekter:
`
Firmenname.ProjektName (Business Logik, Technologieneutral. Keine Abhängigkeit auf ASP.NET oder WPF...)
Firmenname.ProjektName.Data (sofern Data = Datenbank ist)
Firmenname.ProjektName.Data.MsSql (hier ist MsSql Logik mit Dapper)
Firmenname.ProjektName.Data.MsSql.Dapper (nur wenn es mehr als nur Dapper als Endpunkt gibt)
Firmenname.ProjektName.Data.MsSql.EF6 (nur wenn es neben Dapper zB noch EF6 gibt).

// Am Ende dann die Technologien
Firmenname.ProjektName.WebApplication
Firmenname.ProjektName.ConsoleManagementApplication`

Taipi88 Themenstarter:in
1.029 Beiträge seit 2010
vor 6 Jahren

Hi gfoidl,

danke erst mal für deine Hilfe.

Nun - zur Entwurfszeit ist es eigentlich nicht gewünscht - letztendlich soll das dazu dienen eine Anwendung zu haben, bei der man letztendlich per Konfiguration bestimmt mit welcher Datenbank man (auf Basis von Dapper) arbeiten möchte.

Das Thema ist, dass ich in einer JSON-Konfigurationsdatei letztendlich den Namen der Assembly und den Namen des Typs, den ich erstellen möchte eintrage - und dann davon eine Instanz erzeuge - und zwar im Endausbau möglichst so, dass das Projekt gar keine direkte Referenz mehr auf die Library hat.

Der Tipp mit "DependencyContext.Default.GetDefaultAssemblyNames()" war insofern gut, dass ich dort zumindest mal einen Hinweis fand, dass das Programm mal gesagt bekommen hat, dass es eine solche Abhängigkeit gab (in Form eines AssemblyName) - scheitert letztendlich beim Laden der Assembly, da der Compiler natürlich auch keine Kopie der "nicht benötigten" Assembly beigelegt hat. (Gründlich isser ja^^)

Der Typ wird in der Tat mit Assembly.GetType(<name>) erstellt, wofür man nun leider eine geladene Assembly benötigt.

@Abt
Ich verstehe nicht ganz was du hier als Nachteil siehst - im Endeffekt geht es mir darum, dass hier ein Objekt vom Typ IConnectionFactory (MssqlConnectionFactory, OracleConnectionFactory, etc.) per Option für Dapper her soll. Den Austausch der Datenbanktechnologie möchte ich letztendlich per Config-File - und nicht im Code vornehmen. Wie würdest du dieses Ziel anders angehen? Meine direkte Alternative wäre eine Art Factory die allerdings mit MagicStrings arbeiten würde und alle Anbieter kennen müsste, was ich hier für unterlegen halten würde. (Das EntityFramework arbeitet hier ja mit den Providern eigentlich komplett äquivalent wenn ich das richtig sehe...)

Was die Namespace-Aufteilung angeht - kommt sicher auf die Sichtweise an. Der Gedanke ist, dass Frameworks wie Dapper und EF Datenbanken "unter sich vereinen". Sicher - ich hätte das Dapper weglassen können - eventuell probiere ich allerdings auch noch ein anderes Framework aus - dann bin ich froh drum.
(.Data ist übrigens Schnittstellendefinition für RepositoryPattern und UnitOfWork-Pattern, Data.Dapper eine Implementierung, die keinen Wert auf Mssql und ähnliches legt bzw. sich auf Schnittstellen verlässt, die wiederum in Data.Dapper.Mssql und weiteren implementiert werden)

Mein Plan sieht jetzt so aus:
a) AssemblyLoader schreiben, der anhand von "Assembly.GetEntryAssembly().GetReferencedAssemblies()" prüft, ob eine bestimmte Assembly in den Referenzen vorhanden ist
b) Falls dem so ist - wird diese per Assembly.Load geladen, vorgehalten und zurückgegeben
c) Falls nicht, wird der AssemblyLoader (anhand weiterer Config-Einträge) bzw. einer Lookup-Liste von AssemblyNames und Dateinamen die benötigte Assembly per "AssemblyLoadContext.Default.LoadFromAssemblyPath(<pfad>) laden, diese vorhalten und zurückgeben
d) Anschließend wird per Assembly.GetType der Typ erstellt
e) Letztendlich die Instanz per Activator.CreateInstance erstellt

AssemblyLoader würde ich in diesem Fall für einen tollen Kandidaten für das Singleton-Pattern halten.

Wie seht ihr das?

LG

16.807 Beiträge seit 2008
vor 6 Jahren

Warum aber verwendest Du (noch) Reflection und injezierst nicht - wie bei ASP.NET Core mittlerweile vorgesehen, eben zB die entsprechenden Sections?
Microsoft.Extensions.Configuration/

Das Grundlegende Problem ist ja schon bei ASP.NET, dass Du eigene Config Files verwendest.
Das hebelt die Art und Weise aus, wie man bei ASP.NET eben die Konfiguration vornehmen soll: appsettings.json.

Die gesamte Config-Middleware von ASP.NET basiert darauf und Du hebelst es mal kurz aus 😉
zB. ist eine eigene Config eben mit keinerlei Tools wartbar, sei es eben das IIS Config Management, die Azure Config Maske oder auch die AWS Config Maske.
In Docker werden Einstellungen via Environment-Variablen an ASP.NET Core übergeben.
Je nach Stage Deiner Anwendung sind die Settings ja anders; wie wartest und verwaltest Du das?

Du wirfst eine ganze Menge weg, die Du eigentlich geschenkt bekommst.
Ebenso ermöglicht Dir Microsoft.Extensions.Configuration pro Maschine andere Settings zu verwenden, was vor allem während der Entwicklungszeit mit mehreren Entwicklern eines der beliebtesten neuen ASP.NET Core Features sind 😉

Warum nicht einfach das Konfigurationsmodell übernehmen statt das Rad neu zu erfinden?

PS: die Namespaces war nur ein Hinweis.
Die Guidelines sehen das halt anders vor. Aber es sind eben auch "nur" Guidelines.

Taipi88 Themenstarter:in
1.029 Beiträge seit 2010
vor 6 Jahren

Hi Abt,

da haben wir aneinander vorbei geredet. Ich verwende durchaus appsettings.json und auch Umgebungsvariablen die ich über die von dir genannten Sections anspreche.

Im speziellen hab ich dafür eine Klasse die mit Hilfe einer Section die Klasse DapperServiceOptions erstellt und befüllt, welche als Properties:
a) Einen ConnectionString
b) Die IConnectionFactory
bereitstellen soll. Den ConnectionString füllen ist einfach (macht ja das Konfigurationssystem) - wie ich die ConnectionFactory per appsettings.json setzen kann ohne Reflection wie beschrieben zu verwenden ist mir nicht klar.
Hast du unter diesen Gesichtspunkten einen Tipp?

Finde das Konfigurationssystem super und weiß die Vorteile durchaus zu schätzen...

LG

6.911 Beiträge seit 2009
vor 6 Jahren

Hallo Taipi88,

im Endeffekt geht es mir darum, dass hier ein Objekt vom Typ IConnectionFactory (MssqlConnectionFactory, OracleConnectionFactory, etc.) per Option für Dapper her soll.

Warum erwähnst du das essentielle eigentlich nicht bei der ersten Frage?
Das lässt sich doch "einfach" per Factory erledigen. Die Factory kannst du dann per DI injizieren und dann auch zur Laufzeit verschiedene Instanzen der IConnecitonFactory erzeugen. Nur wozu willst du zur Laufzeit verschiedene Connection-Arten haben?

BTW: mit Type.GetType muss die Assembly nicht vorher geladen sein, sie muss nur in einem Ort liegen, den der Assembly-Resolver prüft (probing).

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

16.807 Beiträge seit 2008
vor 6 Jahren

Okay, dann hat mir der wichtigste Kontext offensichtlich gefehlt.
Aber auf Basis dessen sehe ich das wie gfoidl: eine Factory müsste hier doch reichen.