.NET 5 presenta un generador de fuentes. Haremos esto con su ayuda. Este art铆culo cubrir谩 los principales problemas que encontr茅 al usar el generador de fuente y sus soluciones. La generaci贸n de la interfaz de usuario en s铆 est谩 fuera del alcance de este art铆culo. Utilizado por Visual Studio 2019.
Entonces, lo que se requiere para esto:
1. Capacidad para generar archivos js / vue / jsx
2. Acceso al directorio principal del proyecto
3. Acceso al archivo de configuraci贸n
4. Uso de bibliotecas de terceros dentro del generador, por ejemplo Newtonsoft.Json
5. Usando mis otros ensamblajes dentro del generador
6. Acceso a clases / tipos de controladores y modelos ubicados en diferentes ensamblajes
7. Depuraci贸n
Algunas palabras sobre T4
.NET 4.x tiene un generador de c贸digo T4. Inicialmente, trat茅 de resolver mi problema con 茅l. Hubo una serie de problemas, principalmente relacionados con la carga de bibliotecas del sistema, que se resolvieron con diferente 茅xito. Pero cuando se trataba de manejar un ensamblado .NET 5 con controladores, que se refiere a una biblioteca AspNetCore ajena (para el tiempo de ejecuci贸n .NET 4.x), mi cerebro se atasc贸. T4 no quer铆a encontrarlo y cargarlo de ninguna manera.
Estructura del proyecto
Todas las nuevas tecnolog铆as de Microsoft comienzan con Hello World, donde todo funciona bien. Pero cuando comienzas a usarlos en un proyecto real, te encuentras con muchos problemas. Uno de ellos es la estructura del proyecto. En Hello World, este es un ensamblado. Y en un proyecto real hay varios de ellos.
Mi proyecto incluye cuatro ensamblajes condicionales:
1. NetGenerator5.Web - la aplicaci贸n web principal lanzada (net5.0), contiene controladores, un ensamblaje con modelos y el generador mismo est谩n conectados a 茅l.
2. NetGenerator5.Model - ensamblaje con modelos (net5.0)
3. NetGenerator5.Generator - ensamblaje con generador (netstandard2.0)
4. NetGenerator5.Generator.Dependency: ensamblado condicional que se usa dentro del generador (netstandard2.0)
Generador
La clase de generador implementa la interfaz ISourceGenerator con dos m茅todos, Initialize y Execute. El m茅todo Execute se ejecutar谩 directamente durante la compilaci贸n del proyecto al que est谩 conectado el generador.
El propio proyecto generador
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>preview</LangVersion>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" PrivateAssets="all" />
</ItemGroup>
</Project>
驴C贸mo conectarlo? Es necesario en el proyecto principal (NetGenerator5.Web), registrar lo siguiente:
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\NetGenerator5.Generator\NetGenerator5.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
Capacidad para generar archivos js / vue / jsx
Inicialmente, el generador genera archivos cs con c贸digo C #. Para ello, dentro del m茅todo Execute, se utiliza el m茅todo de contexto GeneratorExecutionContext.AddSource. Por lo que tengo entendido, es imposible cambiar su extensi贸n y estos archivos tambi茅n se compilan. Por lo tanto, no es posible poner c贸digo en ning煤n otro idioma all铆. Visual Studio comienza a generar errores de compilaci贸n.
Por lo tanto, necesitamos un enfoque diferente para guardar archivos js / vue / jsx. El habitual System.IO.File.WriteAllText me ayud贸. Pero para esto, necesita saber exactamente d贸nde necesita guardar los archivos generados, es decir, conocer el directorio del proyecto principal.
Acceso al directorio principal del proyecto
Se puede obtener de la siguiente manera:
En el proyecto principal NetGenerator5.Web, escriba lo siguiente:
<ItemGroup>
<CompilerVisibleProperty Include="MSBuildProjectDirectory" />
</ItemGroup>
Esto har谩 visible la variable del sistema para el generador de origen.
Y en el propio generador accederemos a 茅l en el m茅todo Execute de la siguiente manera:
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.MSBuildProjectDirectory", out var projectDirectory)
Adem谩s, necesitamos saber exactamente d贸nde colocar los archivos generados dentro del propio proyecto web (por ejemplo, en wwwroot / js). Se me ocurri贸 pasar esto a trav茅s del archivo de configuraci贸n generatorsettings.json en el proyecto principal. Pero ahora de alguna manera necesito contarle al generador sobre 茅l.
Accediendo al archivo de configuraci贸n
El generador tiene la capacidad de acceder a archivos a trav茅s de la colecci贸n de contexto GeneratorExecutionContext.AdditionalFiles dentro del m茅todo Execute. Para que mi archivo de configuraci贸n est茅 all铆, debe establecer la propiedad de archivo adicional Build Action = C # analyzer en 茅l, o as铆:
<ItemGroup>
<AdditionalFiles Include="generatorsettings.json" />
</ItemGroup>
Despu茅s de eso, el contenido del archivo se puede leer de la siguiente manera
var content = context.AdditionalFiles.First(e => e.Path.EndsWith("generatorsettings.json")).GetText(context.CancellationToken);
A continuaci贸n, surge un problema: es json, pero 驴c贸mo puedo analizarlo?
Usar bibliotecas de terceros dentro del generador
Utilice una biblioteca externa. Por ejemplo Newtonsoft.Json. Aqu铆 es donde algo realmente sali贸 mal. Lo conect茅 a trav茅s de nuget, pero el generador no quer铆a ver esta biblioteca de ninguna manera.
Exception was of type 'FileNotFoundException' with message 'Could not load file or assembly 'Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' or one of its dependencies.
y aunque te rompas.
El libro de cocina tiene una secci贸n dedicada a esto.
Incluso hay un poco m谩s de informaci贸n sobre c贸mo dise帽ar su generador como un paquete nuget. Por alguna raz贸n, no me ayud贸.
Como resultado, al principio me decid铆 de una manera extra帽a. Est煤pidamente agregu茅 la biblioteca directamente al proyecto como un archivo y especifiqu茅 Copiar al directorio de salida = Copiar siempre / Copiar si es m谩s nuevo para 茅l y todo funcion贸. Pero luego obtuve una respuesta a una pregunta en la secci贸n de discusi贸n de Roslyn. El consejo me ayud贸. Es necesario registrarse en el proyecto del generador exactamente as铆:
<ItemGroup>
<!-- Generator dependencies -->
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" GeneratePathProperty="true" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NetGenerator5.Generator.Dependency\NetGenerator5.Generator.Dependency.csproj" />
</ItemGroup>
<PropertyGroup>
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>
<Target Name="GetDependencyTargetPaths">
<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" />
</ItemGroup>
</Target>
O, alternativamente, utilice el System.Text.Json integrado.
Usando mis otros ensamblajes dentro del generador
Adem谩s, ser铆a bueno usar mis otros ensamblajes dentro del generador. Por ejemplo, ser铆a bueno distribuir las clases auxiliares para Vue y React en dos ensamblajes diferentes y conectarlos al generador seg煤n sea necesario.
Por extra帽o que parezca, todo me fue bien aqu铆. Acabo de conectar NetGenerator5.Generator.Dependency a trav茅s de Dependencies - Add Project Reference. Aunque algunos tuvieron problemas.
Acceso a clases / tipos de controladores y modelos ubicados en diferentes ensamblajes
Ahora vayamos a la parte divertida. Para generar archivos, necesitaba acceso a clases / tipos de controladores y modelos. Microsoft recomienda usar SyntaxReceiver
Pero solo tiene acceso a las clases del proyecto compilado actualmente (es decir, en mi caso NetGenerator5.Web), y las clases NetGenerator5.Model no est谩n all铆.
En la misma secci贸n de discusi贸n de Roslyn, se encontr贸 una soluci贸n . Dentro del contexto de GeneratorExecutionContext, est谩 Compilation.GlobalNamespace. Puede repasarlo de forma recursiva y obtener descripciones de todos los tipos, incluido el ensamblado compilado actual y el ensamblado con modelos.
Depuraci贸n
Para depurar, basta con escribir en la clase generadora en el m茅todo Initialize
#if DEBUG
if (!Debugger.IsAttached)
{
Debugger.Launch();
}
#endif
Al iniciar la compilaci贸n del proyecto principal, se abre una ventana con una propuesta para iniciar el depurador. Si hace clic en Aceptar, se iniciar谩 otra instancia de Visual Studio y el modo de depuraci贸n de este generador estar谩 en ella. Puede ir dentro de todas las otras clases y m茅todos, incluso aquellos que est谩n en un ensamblado separado NetGenerator5.Generator.Dependency
Salir
Despu茅s de la compilaci贸n NetGenerator5.Web / wwwroot / js archivo se generated.js y NetGenerator5.Web \ obj \ GeneratedFiles \ NetGenerator5.Generator \ NetGenerator5.Generator.SourceGenerator ARCHIVAREMOS generated.cs ficticias
c贸digo fuente completo se puede ver aqu铆
Fuentes
- github.com/amis92/csharp-source-generators
- github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md
- github.com/dotnet/roslyn/blob/master/docs/features/source-generators.cookbook.md
- mihailromanov.wordpress.com/2021/01/31/net-code-generation-part-6-c-source-generators
- dominikjeske.github.io/source-generators
- github.com/dotnet/roslyn/discussions
- habr.com/ru/post/530454
- habr.com/ru/post/533128