Laden...

Code Generator mit Projekt- und Package-Abhängigkeiten

Erstellt von Palladin007 vor einem Jahr Letzter Beitrag vor einem Jahr 688 Views
Palladin007 Themenstarter:in
2.079 Beiträge seit 2012
vor einem Jahr
Code Generator mit Projekt- und Package-Abhängigkeiten

Guten Abend zusammen,

ich arbeite aktuell an einem Source Generator, der wiederum weitere Projekte benötigt, die wiederum NuGet-Pakete benötigen.
Die eigentliche Funktion läuft, nur der Source Generator beschwert sich immer, dass er seine Abhängigkeiten nicht findet.

Das Problem ist, dass Visual Studio die Source Generator DLL unter in einer Art Cache hält, aber eben nur diese DLL, sonst nichts.
Der Ordner ist bei mir: %temp%\VS\AnalyzerAssemblyLoader\2f880f964e7a46af8432e3c5f292e7f1\7
Dort findet er seine Abhängigkeiten natürlich nicht.

Er scheint generell den Cache zu bevorzugen, wenn er einmal angefangen hat, den Cache zu verwenden, hört er damit nicht mehr auf und ich kann ändern was ich will, er bleibt bei seiner Version im Cache. Ich beende dann immer Visual Studio und lösche den Ordner, aber das kann doch nicht richtig sein?

Ich habe mich an folgenden Beispiel-Projekt orientiert:

https://github.com/dotnet/roslyn-sdk/blob/main/samples/CSharp/SourceGenerators/SourceGeneratorSamples/CSharpSourceGeneratorSamples.csproj

(Kein ISourceGenerator, sondern IIncrementalGenerator)

Sieht also so aus:

<PropertyGroup>
  <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>

<Target Name="GetDependencyTargetPaths">
  <ItemGroup>
    <TargetPathWithTargetPlatformMoniker Include="..\Project1\bin\$(Configuration)\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
    <TargetPathWithTargetPlatformMoniker Include="..\Project2\bin\$(Configuration)\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
    <TargetPathWithTargetPlatformMoniker Include="..\Project3\bin\$(Configuration)\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
    <TargetPathWithTargetPlatformMoniker Include="$(PkgMicrosoft_Bcl_HashCode)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
  </ItemGroup>
</Target>

Hat funktioniert - genau ein mal.

Ich war zwischendurch auch so weit, alles mit ILMerge zusammen zu kleben, was zu sehr schrägen Fehlern geführt hat - Das ILMerge-Package ist ja auch deprecatet.

Die letzte Idee, die ich habe, ist, dass ich alle abhängigen DLLs als EmbeddedResource hinzufüge und dann dann im AppDomain.AssemblyResolve-Event entpacke. Oder nur die NuGet-DLLs als EmbeddedResource und der eigene Code als Link im Projekt?

Aber das muss doch besser/einfacher gehen?
Weiß jemand dazu mehr?

Schon einmal vielen Dank für die Hilfe 😃

D
261 Beiträge seit 2015
vor einem Jahr

siehe Use functionality from NuGet packages

Dazu muss dein Source Generator aber als NuGet Package verpackt sein.

Palladin007 Themenstarter:in
2.079 Beiträge seit 2012
vor einem Jahr

Das hatte ich (glaube ich) ausprobiert, hat aber nur so lange funktioniert, bis Visual Studio seinen Cache benutzt. Wann genau das der Fall ist, konnte ich noch nicht herausfinden.

Ich teste es heute Abend aber nochmal und berichte vom Ergebnis.

Palladin007 Themenstarter:in
2.079 Beiträge seit 2012
vor einem Jahr

Ok, scheinbar funktioniert es doch, allerdings muss ich dafür den ganzen Dependency-Tree der verwendeten NuGet-Packages manuell hinzufügen.
Geht das nicht besser? 😕

Und eine Alternative ohne Package gibt's nicht?
Gerade für's Debuggen wäre es schön, eine Projekt-Referenz zu haben.

Und es gibt noch einen Nachteil:

Der Generator wird als Analyzer gefunden und im VisualStudio angezeigt, die ganzen abhängigen DLLs aber auch.

Palladin007 Themenstarter:in
2.079 Beiträge seit 2012
vor einem Jahr

Ich habe denke ich eine Lösung gefunden:

Jedes andere Projekt, das referenziert wird, braucht:

<PropertyGroup>
  <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>

Und das Generator-Projekt bekommt:

<Target Name="EmbedReferencedAssemblies" AfterTargets="ResolveAssemblyReferences">
    
  <!-- Collect all dependent assemblies -->
  <ItemGroup>
    <FilesToEmbed Include="..\Project1\bin\$(Configuration)\*.dll" />
    <FilesToEmbed Include="..\Project2\bin\$(Configuration)\*.dll" />
    <FilesToEmbed Include="..\Project3\bin\$(Configuration)\*.dll" />
  </ItemGroup>

  <!-- Remove duplicates by file name -->
  <ItemGroup>
    <FilesToEmbedByName Include="%(FilesToEmbed.FileName)" FilePath="%(Identity)" />
  </ItemGroup>

  <RemoveDuplicates Inputs="@(FilesToEmbedByName)">
    <Output TaskParameter="Filtered" ItemName="FilteredFilesToEmbed"/>
  </RemoveDuplicates>

  <ItemGroup>
    <FilesToEmbed Remove="@(FilesToEmbed)" />
    <FilesToEmbed Include="@(FilteredFilesToEmbed->'%(FilePath)')" />
  </ItemGroup>

  <!-- Add filtered files as embedded resource -->
  <ItemGroup>
    <EmbeddedResource Include="@(FilesToEmbed)">
      <LogicalName>%(FilesToEmbed.DestinationSubDirectory)%(FilesToEmbed.Filename)%(FilesToEmbed.Extension)</LogicalName>
    </EmbeddedResource>
  </ItemGroup>
</Target>

Und im Code:

internal static class EmbeddedAssemblyLoader
{
    private static readonly Assembly _assembly = typeof(EmbeddedAssemblyLoader).Assembly;
    private static readonly Dictionary<string, Assembly?> _assemblyCache = new();

    public static void Init()
    {
        AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
    }

    private static Assembly? OnAssemblyResolve(object sender, ResolveEventArgs e)
    {
        var embeddedName = new AssemblyName(e.Name).Name + ".dll";

        if (!_assemblyCache.TryGetValue(embeddedName, out var assembly))
        {
            assembly = LoadEmbeddedAssembly(embeddedName);

            if (assembly is null)
                Debugger.Launch();

            _assemblyCache.Add(embeddedName, assembly);
        }

        return assembly;
    }

    [SuppressMessage("MicrosoftCodeAnalysisCorrectness", "RS1035:Do not use APIs banned for analyzers")]
    private static Assembly? LoadEmbeddedAssembly(string embeddedName)
    {
        var stream = _assembly.GetManifestResourceStream(embeddedName);

        if (stream is null)
            return null;
        var memory = new MemoryStream((int)stream.Length);

        stream.CopyTo(memory);

        return Assembly.Load(memory.ToArray());
    }
}

EmbeddedAssemblyLoader.Init muss dann im statischen Konstruktor des Generators aufgerufen werden.

Ich bin nicht glücklich damit, aber ...

  • es funktioniert sowohl als Package, als auch als normale Projekt-Referenz
  • ich muss nicht den ganzen NuGet-Package-Baum referenzieren, nur um die Assemblies zu includen
  • es kopiert nicht die ganzen anderen DLLs nach "analyzers/dotnet/cs", wo Visual Studio sie ja als Analyzer zu interpretieren versucht.

Zumindest hat es in meinen Tests funktioniert 😃
Das Problem mit dem Cache habe ich aber immer noch, da hilft nur: Cache löschen.