Roslyn ISourceGenerator и DBFirst генерация маппинга FluentNhibernate на лету
Возникла довольно интересная задача используя кодо-генерцию сделать маппинг FluentNhibernate на лету. Порывшись на stackowerflow сам код оказался довольно простым:
private List<string> GetTableNames(string connectionString)
{
using var connection = new NpgsqlConnection(connectionString);
connection.Open();
var tables = new List<string>();
using (var command = new NpgsqlCommand("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'", connection))
using (var reader = command.ExecuteReader())
{`
while (reader.Read())
{
tables.Add(reader.GetString(0));
}
}
return tables;
}
private List<(string Name, string Type)> GetTableColumns(string connectionString, string tableName)
{
using var connection = new NpgsqlConnection(connectionString);
connection.Open();
var columns = new List<(string Name, string Type)>();
using (var command = new NpgsqlCommand($"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '{tableName}'", connection))
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
columns.Add((reader.GetString(0), reader.GetString(1)));
}
}
return columns;
}
private string GenerateClassSource(string tableName, List<(string Name, string Type)> columns)
{
var className = ConvertToClassName(tableName);
var properties = string.Join(Environment.NewLine, columns.Select(c => $"public virtual {ConvertToCSharpType(c.Type)} {c.Name} {{ get; set; }}"));
return $@"
namespace GeneratedClasses
{{
public class {className}
{{
public virtual int Id {{ get; set; }}
{properties}
}}
}}";
}
private string GenerateMappingSource(string tableName, List<(string Name, string Type)> columns)
{
var className = ConvertToClassName(tableName);
var mappings = string.Join(Environment.NewLine, columns.Select(c => $"Map(x => x.{c.Name});"));
return $@"
using FluentNHibernate.Mapping;
namespace GeneratedClasses
{{
public class {className}Map : ClassMap<{className}>
{{
public {className}Map()
{{
Id(x => x.Id);
Table(""{tableName}"");
{mappings}
}}
}}
}}";
}
private string ConvertToClassName(string tableName)
{
return char.ToUpper(tableName[0]) + tableName.Substring(1);
}
private string ConvertToCSharpType(string sqlType)
{
return sqlType switch
{
"integer" => "int",
"serial" => "int",
"bigint" => "long",
"boolean" => "bool",
"text" => "string",
"timestamp without time zone" => "DateTime",
_ => "string"
};
}
Однако, возникла проблема. Простая, базовая кодо-генерация работала, но генерация маппинга таблиц - нет. При том что в консольной версии генерация маппинга была рабочей.
Для отладки использовал следующий трюк:
while (!System.Diagnostics.Debugger.IsAttached)
System.Threading.Thread.Sleep(500);
Добавил ожидание подключение отладчика при инициализации.
Оказалось, падение происходит, из за того, что не смог загрузить библиотеку
Could not load file or assembly 'Npgsql, Version=5.0.18.0, Culture=neutral, PublicKeyToken=5d8b90d52f46fda7'.
Пробуем сделать перехват загрузки dll
public void Initialize(GeneratorInitializationContext context)
{
AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
}
private Assembly ResolveAssembly(object sender, ResolveEventArgs args)
{
// Логика для нахождения и загрузки нужной сборки
var assemblyName = new AssemblyName(args.Name).Name + ".dll";
var s = "D:\\mypath\\";
var assemblyPath = Path.Combine(s, assemblyName);
_sources.Add(assemblyPath);
return Assembly.LoadFrom(assemblyPath);
}
Теперь не загружается
Could not load file or assembly 'Microsoft.Bcl.AsyncInterfaces, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'
Оказывается сборка копируется в папку
C:\Users\artem\AppData\Local\Temp\VBCSCompiler\AnalyzerAssemblyLoader\ea17e61fb2a544369730b7d0a35a52d9\5\
А что если добавлять не проект, а самостоятельно собрать и подключить как внешний analyzer?
Создаем Directory.Build.targets
<Project>
<!-- Установите директорию для анализаторов -->
<PropertyGroup>
<AnalyzerDirectory>с://Generator/SourceGenerator/bin/Debug/netstandard2.0/</AnalyzerDirectory>
</PropertyGroup>
<!-- Включите все DLL в указанной директории как анализаторы -->
<ItemGroup>
<Analyzer Include="$(AnalyzerDirectory)\**\SourceGenerator.dll" />
</ItemGroup>
<!-- Дополнительные настройки или цели, если необходимо -->
<Target Name="BeforeCoreCompile" BeforeTargets="CoreCompile">
<Message Text="Используются анализаторы из директории: $(AnalyzerDirectory)" Importance="high" />
</Target>
</Project>
И.. снова неудача! Он все равно копирует dll во временную папку и падает из-за конфликта Microsoft.Bcl.AsyncInterfaces
Что же делать? А решение оказалось совсем простым, можно было просто сделать монолитную сборку и все конфликты невозможны технически
Используем nuget пакет FodyWeavers
И добавляем в настройку проекта по генерации
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>
Теперь все работает. Надеюсь, сэкономил время, для тех, кто мог столкнуться с похожей проблемой
Код проекта на github: https://github.com/ArtemRuticker/ykovalenko