NpgsqlConnection не считывает столбцы jsonb для унаследованных классов DTO

#c# #postgresql #dapper #npgsql

#c# #postgresql #dapper #npgsql

Вопрос:

У меня здесь минимальное воспроизведение https://github.com/PaloMraz/ReadJsonbArraysWithDtoInheritance/blob/master/Program.cs . По сути, у меня есть таблица со столбцом jsonb, сопоставленным с массивом DTO, использующим пользовательский Dapper TypeHandler , подобный этому:

   static async Task Main(string[] args)
  {
    DefaultTypeMap.MatchNamesWithUnderscores = true;
    SqlMapper.AddTypeHandler(new NoteListTypeMapper());

    const string ConnectionString = "Host=localhost;Username=postgres;Port=5432;Password=postgres;Database=postgres";
    using(var connection = new NpgsqlConnection(ConnectionString))
    {
      await connection.OpenAsync(); // make sure all the code runs in the same session...

      await connection.ExecuteAsync("create temporary table product(id serial primary key, name text, notes jsonb);");
      await connection.ExecuteAsync("insert into product (name, notes) values ('ProductA', '[{"id": 1, "content": "Note1 - A"}, {"id": 2, "content": "Note2 - A"} ]'::jsonb);");
      await connection.ExecuteAsync("insert into product (name, notes) values ('ProductB', '[{"id": 2, "content": "Note1 - B"}, {"id": 2, "content": "Note2 - B"} ]'::jsonb);");

      // This loads the Notes jsonb column fine.
      List<Product> products = (await connection.QueryAsync<Product>("select * from product;")).ToList();
      Console.WriteLine(products[0]); // Prints: Product(1, ProductA): Note(1, Note1 - A), Note(2, Note2 - A)
      Console.WriteLine(products[1]); // Prints: Product(2, ProductB): Note(2, Note1 - B), Note(2, Note2 - B)

      // This does NOT load the Notes jsonb colum.
      List<ProductView> productViews = (await connection.QueryAsync<ProductView>("select * from product;")).ToList();
      Console.WriteLine(productViews[0]); // Prints: Product(1, ProductA):
      Console.WriteLine(productViews[1]); // Prints: Product(2, ProductB):
    }
  }

  internal class NoteListTypeMapper : SqlMapper.TypeHandler<IList<Note>>
  {
    public override IList<Note> Parse(object value)
    {
      if (value is string json amp;amp; !string.IsNullOrEmpty(json))
      {
        return JsonConvert.DeserializeObject<IList<Note>>(json);
      }
      else
      {
        return new List<Note>();
      }
    }


    public override void SetValue(IDbDataParameter parameter, IList<Note> value)
    {
      parameter.Value = JsonConvert.SerializeObject(value);
      if (parameter is NpgsqlParameter postgresParameter)
      {
        postgresParameter.NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Jsonb;
      }
    }
  }
}

public class Note
{
  public int Id { get; set; }
  public string Content { get; set; } = "";
  public override string ToString() => $"Note({this.Id}, {this.Content})";
}


public class Product
{
  public int Id { get; set; }
  public string Name { get; set; } = "";
  public IList<Note> Notes { get; } = new List<Note>();

  public override string ToString() => $"Product({this.Id}, {this.Name}): "   string.Join(", ", this.Notes);
}


public class ProductView : Product
{
  public string Manufacturer { get; set; } = "";
}
  

Проблема в том, что при запросе с типом продукта connection.QueryAsync<Product> это работает (встроенный список заметок десериализуется правильно), но при использовании типа connection.QueryAsync<ProductView> ProductView это не работает (поле Notes пустое).

Это должно работать? Я что-то упускаю?

Спасибо за вашу помощь!

С уважением,

Palo

Ответ №1:

Обнаружена причина; быстрое решение проблемы — сделать свойство коллекции настраиваемым, как указано в комментарии к Product.Notes свойству в обновленном репозитории GitHub https://github.com/PaloMraz/ReadJsonbArraysWithDtoInheritance:

 /// <summary>
/// If this is read-only, Dapper will not deserialize the property from DataReader for subclasses,
/// e.g <see cref="ProductView"/>. This is because Dapper internally tries to deserialize the compiler-
/// generated backing field, which is present only in the base class. When the property is settable
/// (even with private setter!), Dapper deserializes it correctly for derived classes also.
/// </summary>
public IList<Note> Notes { get; private set; } = new List<Note>();