Как стать автором
Обновить

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

Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.