Multi-Mapper для создания иерархии объектов
Я немного поигрался с этим, потому что кажется, что это похоже на пример документированных постов / пользователей, но немного отличается и не работает для меня.
Предполагая следующую упрощенную настройку (у контакта есть несколько телефонных номеров):
public class Contact
{
public int ContactID { get; set; }
public string ContactName { get; set; }
public IEnumerable<Phone> Phones { get; set; }
}
public class Phone
{
public int PhoneId { get; set; }
public int ContactID { get; set; } // foreign key
public string Number { get; set; }
public string Type { get; set; }
public bool IsActive { get; set; }
}
Я хотел бы закончить тем, что возвращает контакт с несколькими объектами Phone. Таким образом, если бы у меня было 2 контакта, по 2 телефона в каждом, мой SQL возвращал бы соединение из них в виде результирующего набора с 4 строками. Затем Даппер выскакивает 2 контактных объекта с двумя телефонами каждый.
Вот SQL в хранимой процедуре:
SELECT *
FROM Contacts
LEFT OUTER JOIN Phones ON Phones.ReferenceId=Contacts.ReferenceId
WHERE clientid=1
Я попробовал это, но в итоге получилось 4 кортежа (что нормально, но не то, на что я надеялся... это просто означает, что мне все еще нужно повторно нормализовать результат):
var x = cn.Query<Contact, Phone, Tuple<Contact, Phone>>("sproc_Contacts_SelectByClient",
(co, ph) => Tuple.Create(co, ph),
splitOn: "PhoneId", param: p,
commandType: CommandType.StoredProcedure);
и когда я пытаюсь использовать другой метод (ниже), я получаю исключение "Невозможно привести объект типа" System.Int32 "к типу" System.Collections.Generic.IEnumerable`1[Phone]'"."
var x = cn.Query<Contact, IEnumerable<Phone>, Contact>("sproc_Contacts_SelectByClient",
(co, ph) => { co.Phones = ph; return co; },
splitOn: "PhoneId", param: p,
commandType: CommandType.StoredProcedure);
Я просто что-то делаю не так? Это похоже на пример posts/owner, за исключением того, что я иду от родителя к ребенку, а не от ребенка к родителю.
заранее спасибо
9 ответов
Вы не делаете ничего плохого, это просто не тот способ, которым был разработан API. Все Query
API всегда будут возвращать объект на строку базы данных.
Таким образом, это хорошо работает для многих -> одного направления, но менее хорошо для одного -> множества карт.
Здесь есть 2 вопроса:
Если мы представим встроенный преобразователь, который работает с вашим запросом, мы должны были бы "отбросить" дублирующиеся данные. (Контакты.* Дублируется в вашем запросе)
Если мы разработаем его для работы с парой один -> много, нам понадобится какая-то карта идентичности. Что добавляет сложности.
Возьмем, к примеру, этот запрос, который эффективен, если вам просто нужно извлечь ограниченное количество записей, если вы увеличите число до миллиона, что станет сложнее, потому что вам нужно выполнять потоковую передачу и не можете загрузить все в память:
var sql = "set nocount on
DECLARE @t TABLE(ContactID int, ContactName nvarchar(100))
INSERT @t
SELECT *
FROM Contacts
WHERE clientid=1
set nocount off
SELECT * FROM @t
SELECT * FROM Phone where ContactId in (select t.ContactId from @t t)"
Что вы могли бы сделать, это продлить GridReader
чтобы разрешить переназначение:
var mapped = cnn.QueryMultiple(sql)
.Map<Contact,Phone, int>
(
contact => contact.ContactID,
phone => phone.ContactID,
(contact, phones) => { contact.Phones = phones };
);
Предполагая, что вы расширяете свой GridReader и с помощью картографа:
public static IEnumerable<TFirst> Map<TFirst, TSecond, TKey>
(
this GridReader reader,
Func<TFirst, TKey> firstKey,
Func<TSecond, TKey> secondKey,
Action<TFirst, IEnumerable<TSecond>> addChildren
)
{
var first = reader.Read<TFirst>().ToList();
var childMap = reader
.Read<TSecond>()
.GroupBy(s => secondKey(s))
.ToDictionary(g => g.Key, g => g.AsEnumerable());
foreach (var item in first)
{
IEnumerable<TSecond> children;
if(childMap.TryGetValue(firstKey(item), out children))
{
addChildren(item,children);
}
}
return first;
}
Поскольку это немного сложнее и сложнее, с оговорками. Я не склоняюсь к тому, чтобы включить это в ядро.
К вашему сведению, я получил ответ Сэма, сделав следующее:
Сначала я добавил файл класса с именем "Extensions.cs". Мне пришлось изменить ключевое слово "this" на "reader" в двух местах:
using System;
using System.Collections.Generic;
using System.Linq;
using Dapper;
namespace TestMySQL.Helpers
{
public static class Extensions
{
public static IEnumerable<TFirst> Map<TFirst, TSecond, TKey>
(
this Dapper.SqlMapper.GridReader reader,
Func<TFirst, TKey> firstKey,
Func<TSecond, TKey> secondKey,
Action<TFirst, IEnumerable<TSecond>> addChildren
)
{
var first = reader.Read<TFirst>().ToList();
var childMap = reader
.Read<TSecond>()
.GroupBy(s => secondKey(s))
.ToDictionary(g => g.Key, g => g.AsEnumerable());
foreach (var item in first)
{
IEnumerable<TSecond> children;
if (childMap.TryGetValue(firstKey(item), out children))
{
addChildren(item, children);
}
}
return first;
}
}
}
Во-вторых, я добавил следующий метод, изменив последний параметр:
public IEnumerable<Contact> GetContactsAndPhoneNumbers()
{
var sql = @"
SELECT * FROM Contacts WHERE clientid=1
SELECT * FROM Phone where ContactId in (select ContactId FROM Contacts WHERE clientid=1)";
using (var connection = GetOpenConnection())
{
var mapped = connection.QueryMultiple(sql)
.Map<Contact,Phone, int> (
contact => contact.ContactID,
phone => phone.ContactID,
(contact, phones) => { contact.Phones = phones; }
);
return mapped;
}
}
Проверьте https://www.tritac.com/blog/dappernet-by-example/ Вы можете сделать что-то вроде этого:
public class Shop {
public int? Id {get;set;}
public string Name {get;set;}
public string Url {get;set;}
public IList<Account> Accounts {get;set;}
}
public class Account {
public int? Id {get;set;}
public string Name {get;set;}
public string Address {get;set;}
public string Country {get;set;}
public int ShopId {get;set;}
}
var lookup = new Dictionary<int, Shop>()
conn.Query<Shop, Account, Shop>(@"
SELECT s.*, a.*
FROM Shop s
INNER JOIN Account a ON s.ShopId = a.ShopId
", (s, a) => {
Shop shop;
if (!lookup.TryGetValue(s.Id, out shop)) {
lookup.Add(s.Id, shop = s);
}
shop.Accounts.Add(a);
return shop;
},
).AsQueryable();
var resultList = lookup.Values;
Я получил это из тестов dapper.net: https://code.google.com/p/dapper-dot-net/source/browse/Tests/Tests.cs#1343
Поддержка нескольких результирующих наборов
В вашем случае было бы намного лучше (и также проще) иметь запрос с несколькими наборами результатов. Это просто означает, что вы должны написать два оператора select:
- Тот, который возвращает контакты
- И тот, который возвращает свои номера телефонов
Таким образом, ваши объекты будут уникальными и не будут дублироваться.
Вот многоразовое решение, которое довольно легко использовать. Это небольшая модификация ответа Эндрюса.
public static IEnumerable<TParent> QueryParentChild<TParent, TChild, TParentKey>(
this IDbConnection connection,
string sql,
Func<TParent, TParentKey> parentKeySelector,
Func<TParent, IList<TChild>> childSelector,
dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
{
Dictionary<TParentKey, TParent> cache = new Dictionary<TParentKey, TParent>();
connection.Query<TParent, TChild, TParent>(
sql,
(parent, child) =>
{
if (!cache.ContainsKey(parentKeySelector(parent)))
{
cache.Add(parentKeySelector(parent), parent);
}
TParent cachedParent = cache[parentKeySelector(parent)];
IList<TChild> children = childSelector(cachedParent);
children.Add(child);
return cachedParent;
},
param as object, transaction, buffered, splitOn, commandTimeout, commandType);
return cache.Values;
}
Пример использования
public class Contact
{
public int ContactID { get; set; }
public string ContactName { get; set; }
public List<Phone> Phones { get; set; } // must be IList
public Contact()
{
this.Phones = new List<Phone>(); // POCO is responsible for instantiating child list
}
}
public class Phone
{
public int PhoneID { get; set; }
public int ContactID { get; set; } // foreign key
public string Number { get; set; }
public string Type { get; set; }
public bool IsActive { get; set; }
}
conn.QueryParentChild<Contact, Phone, int>(
"SELECT * FROM Contact LEFT OUTER JOIN Phone ON Contact.ContactID = Phone.ContactID",
contact => contact.ContactID,
contact => contact.Phones,
splitOn: "PhoneId");
Основанный на подходе Сэма Шафрона (и Майка Глисона), вот решение, которое позволит использовать несколько детей и несколько уровней.
using System;
using System.Collections.Generic;
using System.Linq;
using Dapper;
namespace TestMySQL.Helpers
{
public static class Extensions
{
public static IEnumerable<TFirst> MapChild<TFirst, TSecond, TKey>
(
this SqlMapper.GridReader reader,
List<TFirst> parent,
List<TSecond> child,
Func<TFirst, TKey> firstKey,
Func<TSecond, TKey> secondKey,
Action<TFirst, IEnumerable<TSecond>> addChildren
)
{
var childMap = child
.GroupBy(secondKey)
.ToDictionary(g => g.Key, g => g.AsEnumerable());
foreach (var item in parent)
{
IEnumerable<TSecond> children;
if (childMap.TryGetValue(firstKey(item), out children))
{
addChildren(item, children);
}
}
return parent;
}
}
}
Затем вы можете прочитать его вне функции.
using (var multi = conn.QueryMultiple(sql))
{
var contactList = multi.Read<Contact>().ToList();
var phoneList = multi.Read<Phone>().ToList;
contactList = multi.MapChild
(
contactList,
phoneList,
contact => contact.Id,
phone => phone.ContactId,
(contact, phone) => {contact.Phone = phone;}
).ToList();
return contactList;
}
Затем можно снова вызвать функцию map для следующего дочернего объекта, используя тот же родительский объект. Вы также можете реализовать разбиение на родительские или дочерние операторы чтения независимо от функции map.
Вот дополнительный метод расширения "от одного до N"
public static TFirst MapChildren<TFirst, TSecond, TKey>
(
this SqlMapper.GridReader reader,
TFirst parent,
IEnumerable<TSecond> children,
Func<TFirst, TKey> firstKey,
Func<TSecond, TKey> secondKey,
Action<TFirst, IEnumerable<TSecond>> addChildren
)
{
if (parent == null || children == null || !children.Any())
{
return parent;
}
Dictionary<TKey, IEnumerable<TSecond>> childMap = children
.GroupBy(secondKey)
.ToDictionary(g => g.Key, g => g.AsEnumerable());
if (childMap.TryGetValue(firstKey(parent), out IEnumerable<TSecond> foundChildren))
{
addChildren(parent, foundChildren);
}
return parent;
}
Однажды мы решили переместить наш DataAccessLayer в хранимые процедуры, и эти процедуры часто возвращают несколько связанных результатов (пример ниже).
Что ж, мой подход почти такой же, но, может быть, немного удобнее.
Вот как может выглядеть ваш код:
using ( var conn = GetConn() )
{
var res = await conn
.StoredProc<Person>( procName, procParams )
.Include<Book>( ( p, b ) => p.Books = b.Where( x => x.PersonId == p.Id ).ToList() )
.Include<Course>( ( p, c ) => p.Courses = c.Where( x => x.PersonId == p.Id ).ToList() )
.Include<Course, Mark>( ( c, m ) => c.Marks = m.Where( x => x.CourseId == c.Id ).ToList() )
.Execute();
}
Давайте разберемся...
Расширение:
public static class SqlExtensions
{
public static StoredProcMapper<T> StoredProc<T>( this SqlConnection conn, string procName, object procParams )
{
return StoredProcMapper<T>
.Create( conn )
.Call( procName, procParams );
}
}
Картограф:
public class StoredProcMapper<T>
{
public static StoredProcMapper<T> Create( SqlConnection conn )
{
return new StoredProcMapper<T>( conn );
}
private List<MergeInfo> _merges = new List<MergeInfo>();
public SqlConnection Connection { get; }
public string ProcName { get; private set; }
public object Parameters { get; private set; }
private StoredProcMapper( SqlConnection conn )
{
Connection = conn;
_merges.Add( new MergeInfo( typeof( T ) ) );
}
public StoredProcMapper<T> Call( object procName, object parameters )
{
ProcName = procName.ToString();
Parameters = parameters;
return this;
}
public StoredProcMapper<T> Include<TChild>( MergeDelegate<T, TChild> mapper )
{
return Include<T, TChild>( mapper );
}
public StoredProcMapper<T> Include<TParent, TChild>( MergeDelegate<TParent, TChild> mapper )
{
_merges.Add( new MergeInfo<TParent, TChild>( mapper ) );
return this;
}
public async Task<List<T>> Execute()
{
if ( string.IsNullOrEmpty( ProcName ) )
throw new Exception( $"Procedure name not specified! Please use '{nameof(Call)}' method before '{nameof( Execute )}'" );
var gridReader = await Connection.QueryMultipleAsync(
ProcName, Parameters, commandType: CommandType.StoredProcedure );
foreach ( var merge in _merges )
{
merge.Result = gridReader
.Read( merge.Type )
.ToList();
}
foreach ( var merge in _merges )
{
if ( merge.ParentType == null )
continue;
var parentMerge = _merges.FirstOrDefault( x => x.Type == merge.ParentType );
if ( parentMerge == null )
throw new Exception( $"Wrong parent type '{merge.ParentType.FullName}' for type '{merge.Type.FullName}'." );
foreach ( var parent in parentMerge.Result )
{
merge.Merge( parent, merge.Result );
}
}
return _merges
.First()
.Result
.Cast<T>()
.ToList();
}
private class MergeInfo
{
public Type Type { get; }
public Type ParentType { get; }
public IEnumerable Result { get; set; }
public MergeInfo( Type type, Type parentType = null )
{
Type = type;
ParentType = parentType;
}
public void Merge( object parent, IEnumerable children )
{
MergeInternal( parent, children );
}
public virtual void MergeInternal( object parent, IEnumerable children )
{
}
}
private class MergeInfo<TParent, TChild> : MergeInfo
{
public MergeDelegate<TParent, TChild> Action { get; }
public MergeInfo( MergeDelegate<TParent, TChild> mergeAction )
: base( typeof( TChild ), typeof( TParent ) )
{
Action = mergeAction;
}
public override void MergeInternal( object parent, IEnumerable children )
{
Action( (TParent)parent, children.Cast<TChild>() );
}
}
public delegate void MergeDelegate<TParent, TChild>( TParent parent, IEnumerable<TChild> children );
}
Вот и все, но если вы хотите провести быстрый тест, вот модели и процедуры для вас:
Модели:
public class Person
{
public Guid Id { get; set; }
public string Name { get; set; }
public List<Course> Courses { get; set; }
public List<Book> Books { get; set; }
public override string ToString() => Name;
}
public class Book
{
public Guid Id { get; set; }
public Guid PersonId { get; set; }
public string Name { get; set; }
public override string ToString() => Name;
}
public class Course
{
public Guid Id { get; set; }
public Guid PersonId { get; set; }
public string Name { get; set; }
public List<Mark> Marks { get; set; }
public override string ToString() => Name;
}
public class Mark
{
public Guid Id { get; set; }
public Guid CourseId { get; set; }
public int Value { get; set; }
public override string ToString() => Value.ToString();
}
СП:
if exists (
select *
from sysobjects
where
id = object_id(N'dbo.MultiTest')
and ObjectProperty( id, N'IsProcedure' ) = 1 )
begin
drop procedure dbo.MultiTest
end
go
create procedure dbo.MultiTest
@PersonId UniqueIdentifier
as
begin
declare @tmpPersons table
(
Id UniqueIdentifier,
Name nvarchar(50)
);
declare @tmpBooks table
(
Id UniqueIdentifier,
PersonId UniqueIdentifier,
Name nvarchar(50)
)
declare @tmpCourses table
(
Id UniqueIdentifier,
PersonId UniqueIdentifier,
Name nvarchar(50)
)
declare @tmpMarks table
(
Id UniqueIdentifier,
CourseId UniqueIdentifier,
Value int
)
--------------------------------------------------
insert into @tmpPersons
values
( '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Иван' ),
( '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Василий' ),
( '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Алефтина' )
insert into @tmpBooks
values
( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Математика' ),
( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Физика' ),
( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Геометрия' ),
( NewId(), '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Книга Биология' ),
( NewId(), '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Книга Химия' ),
( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга История' ),
( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга Литература' ),
( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга Древне-шумерский диалект иврита' )
insert into @tmpCourses
values
( '30945b68-a6ef-4da8-9a35-d3b2845e7de3', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Математика' ),
( '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Физика' ),
( '92bbefd1-9fec-4dc7-bb58-986eadb105c8', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Геометрия' ),
( '923a2f0c-c5c7-4394-847c-c5028fe14711', '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Биология' ),
( 'ace50388-eb05-4c46-82a9-5836cf0c988c', '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Химия' ),
( '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'История' ),
( '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Литература' ),
( '73ac366d-c7c2-4480-9513-28c17967db1a', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Древне-шумерский диалект иврита' )
insert into @tmpMarks
values
( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 98 ),
( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 87 ),
( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 76 ),
( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 89 ),
( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 78 ),
( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 67 ),
( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 79 ),
( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 68 ),
( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 75 ),
----------
( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 198 ),
( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 187 ),
( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 176 ),
( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 189 ),
( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 178 ),
( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 167 ),
----------
( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 8 ),
( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 7 ),
( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 6 ),
( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 9 ),
( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 8 ),
( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 7 ),
( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 9 ),
( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 8 ),
( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 5 )
--------------------------------------------------
select * from @tmpPersons
select * from @tmpBooks
select * from @tmpCourses
select * from @tmpMarks
end
go
Я хотел поделиться своим решением этой проблемы и посмотреть, есть ли у кого-нибудь конструктивные отзывы о подходе, который я использовал?
В проекте, над которым я работаю, у меня есть несколько требований, которые мне нужно сначала объяснить:
- Я должен сохранять свои POCO как можно более чистыми, так как эти классы будут публично предоставлены в обертке API.
- Мои POCO находятся в отдельной библиотеке классов из-за вышеуказанного требования
- Будет несколько уровней иерархии объектов, которые будут варьироваться в зависимости от данных (поэтому я не могу использовать Generic Type Mapper, или мне придется написать тонны из них, чтобы удовлетворить все возможные ситуации)
Итак, я сделал так, чтобы SQL обрабатывал иерархию 2-го уровня, возвращая одну строку JSON в виде столбца в исходной строке следующим образом (для иллюстрации удалили другие столбцы / свойства и т. Д.):
Id AttributeJson
4 [{Id:1,Name:"ATT-NAME",Value:"ATT-VALUE-1"}]
Затем мои POCO создаются следующим образом:
public abstract class BaseEntity
{
[KeyAttribute]
public int Id { get; set; }
}
public class Client : BaseEntity
{
public List<ClientAttribute> Attributes{ get; set; }
}
public class ClientAttribute : BaseEntity
{
public string Name { get; set; }
public string Value { get; set; }
}
Где POCO наследуется от BaseEntity. (Для иллюстрации я выбрал довольно простую одноуровневую иерархию, как показано в свойстве "Атрибуты" клиентского объекта.)
Затем у меня на уровне данных следующий "класс данных", который наследуется от POCO Client
,
internal class dataClient : Client
{
public string AttributeJson
{
set
{
Attributes = value.FromJson<List<ClientAttribute>>();
}
}
}
Как вы можете видеть выше, происходит то, что SQL возвращает столбец с именем "AttributeJson", который сопоставляется со свойством AttributeJson
в классе dataClient. Это только установщик, который десериализует JSON в Attributes
имущество по наследству Client
учебный класс. Класс dataClient internal
к уровню доступа к данным и ClientProvider
(моя фабрика данных) возвращает исходное клиентское POCO вызывающему приложению / библиотеке следующим образом:
var clients = _conn.Get<dataClient>();
return clients.OfType<Client>().ToList();
Обратите внимание, что я использую Dapper.Contrib и добавил новый Get<T>
Метод, который возвращает IEnumerable<T>
Есть несколько вещей, которые следует отметить с этим решением:
С сериализацией JSON очевидна компромисс производительности - я сравнил это с 1050 строками с 2 подпрограммами
List<T>
свойства, каждое из которых имеет 2 сущности в списке и имеет тактовую частоту 279 мс - что является приемлемым для потребностей моих проектов - это также с ZERO-оптимизацией на стороне SQL, поэтому я смогу побриться там на несколько мс.Это означает, что для создания JSON для каждого требуемого требуются дополнительные запросы SQL.
List<T>
свойство, но, опять же, это меня устраивает, так как я знаю SQL очень хорошо и не очень хорошо разбираюсь в динамике / отражении и т. д., поэтому я чувствую, что у меня больше контроля над вещами, поскольку я действительно понимаю, что происходит под капотом:-)
Вполне возможно, что есть лучшее решение, чем это, и если оно будет, я был бы очень признателен, если бы вы услышали ваши мысли - это просто решение, которое я придумал, которое до сих пор соответствует моим потребностям в этом проекте (хотя это экспериментально на стадии публикации).).
использовать это:
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public Category Category { get; set; }
}
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public ICollection<Product> Products { get; set; }
}
using (var connection = new SQLiteConnection(connString))
{
var sql = @"select productid, productname, p.categoryid, categoryname
from products p
inner join categories c on p.categoryid = c.categoryid";
var products = await connection.QueryAsync<Product, Category, Product>(sql, (product, category) => {
product.Category = category;
return product;
},
splitOn: "CategoryId");
products.ToList().ForEach(product => Console.WriteLine($"Product: {product.ProductName}, Category: {product.Category.CategoryName}"));
Console.ReadLine();
}
взято из: Управление отношениями