I've been playing around with this for a bit, because it seems like it feels a lot like the documented posts/users example, but its slightly different and isn't working for me.
Assuming the following simplified setup (a contact has multiple phone numbers):
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; }
}
I'd love to end up with something that returns a Contact with multiple Phone objects. That way, if I had 2 contacts, with 2 phones each, my SQL would return a join of those as a result set with 4 total rows. Then Dapper would pop out 2 contact objects with two phones each.
Here is the SQL in the stored procedure:
SELECT *
FROM Contacts
LEFT OUTER JOIN Phones ON Phones.ReferenceId=Contacts.ReferenceId
WHERE clientid=1
I tried this, but ended up with 4 Tuples (which is OK, but not what I was hoping for... it just means I still have to re-normalize the result):
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);
and when I try another method (below), I get an exception of "Unable to cast object of type 'System.Int32' to type '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);
Am I just doing something wrong? It seems just like the posts/owner example, except that I'm going from the parent to the child instead of the child to the parent.
You are doing nothing wrong, it is just not the way the API was designed. All the Query APIs will always return an object per database row.
So, this works well on the many -> one direction, but less well for the one -> many multi-map.
There are 2 issues here:
If we introduce a built-in mapper that works with your query, we would be expected to "discard" duplicate data. (Contacts.* is duplicated in your query)
If we design it to work with a one -> many pair, we will need some sort of identity map. Which adds complexity.
Take for example this query which is efficient if you just need to pull a limited number of records, if you push this up to a million stuff get trickier, cause you need to stream and can not load everything into memory:
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)"
What you could do is extend the GridReader to allow for the remapping:
var mapped = cnn.QueryMultiple(sql)
.Map<Contact,Phone, int>
(
contact => contact.ContactID,
phone => phone.ContactID,
(contact, phones) => { contact.Phones = phones };
);
Assuming you extend your GridReader and with a mapper:
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;
}
Since this is a bit tricky and complex, with caveats. I am not leaning towards including this in core.
FYI - I got Sam's answer working by doing the following:
First, I added a class file called "Extensions.cs". I had to change the "this" keyword to "reader" in two places:
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;
}
}
}
Second, I added the following method, modifying the last parameter:
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;
}
}
Check out https://www.tritac.com/blog/dappernet-by-example/
You could do something like this:
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;
I got this from the dapper.net tests: https://code.google.com/p/dapper-dot-net/source/browse/Tests/Tests.cs#1343
Multi result set support
In your case it would be much better (and easier as well) to have a multi resultset query. This simply means that you should write two select statements:
One that returns contacts
And one that returns their phone numbers
This way your objects would be unique and wouldn't duplicate.
Here's a reusable solution that is pretty easy to use. It is a slight modification of Andrews answer.
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;
}
Example usage
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");
Based on Sam Saffron's (and Mike Gleason's) approach, here is a solution which will allow for multiple children and multiple levels.
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;
}
}
}
Then you can have it read outside of the function.
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;
}
The map function can then be called again for the next child object using the same parent object. You can also implement splits on the parent or child read statements independently of the map function.
Here is a 'single to N' additional extension method
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;
}
Once we decided to move our DataAccessLayer to stored procedures, and these procedures are often return multiple linked results (example below).
Well, my approach is almost the same, but maybe little bit more comfortable.
This is how your code may look like:
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();
}
Let's break it down...
Extension:
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 );
}
}
Mapper:
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 );
}
That's all, but if you wanna do quick test, here is models and procedure for you:
Models:
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();
}
SP:
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
I wanted to share my solution to this issue and see if anyone has any constructive feedback on the approach I've used?
I have a few requirements in the project I'm working on which I need to explain first up:
I have to keep my POCO's as clean as possible as these classes will be publicly shared in an API wrapper.
My POCO's are in a seperate Class Library because of the above requirement
There are going to be multiple object hierarchy levels that will vary depending on data (so I cannot use a Generic Type Mapper or I'd have to write tons of them to cater for all possible eventualities)
So, what I have done is to get SQL to handle the 2nd - nth Level heirarchy by returning a Single JSON string as a column on the original row as follows (stripped out the other columns / properties etc to illustrate):
Id AttributeJson
4 [{Id:1,Name:"ATT-NAME",Value:"ATT-VALUE-1"}]
Then, my POCO's are built up like the below :
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; }
}
Where the POCO's inherit from BaseEntity. (To illustrate I've chosen a fairly simple, single level heirarchy as shown by the "Attributes" property of the client object. )
I then have in my Data Layer the following "Data Class" which inherits from the POCO Client.
internal class dataClient : Client
{
public string AttributeJson
{
set
{
Attributes = value.FromJson<List<ClientAttribute>>();
}
}
}
As you can see above, whats happening is that SQL is returning a column called "AttributeJson" which is mapped to the property AttributeJson in the dataClient class. This has only a setter which deserialises the JSON to the Attributes property on the inherited Client class. The dataClient Class is internal to the Data Access Layer and the ClientProvider (my data factory) returns the original Client POCO to the calling App / Library like so :
var clients = _conn.Get<dataClient>();
return clients.OfType<Client>().ToList();
Note that I'm using Dapper.Contrib and have added a new Get<T> Method that returns an IEnumerable<T>
There are a couple things to note with this solution:
There's an obvious performance trade off with the JSON serialisation - I've benchmarked this against 1050 rows with 2 sub List<T> properties, each with 2 entities in the list and it clocks in at 279ms - which is acceptable for my projects needs - this is also with ZERO optimisation on the SQL side of things so I should be able to shave a few ms there.
It does mean additional SQL queries are required to build up the JSON for each required List<T> property, but again, this suits me as I know SQL pretty well and am not so fluent on dynamics / reflection etc.. so this way I feel like I have more control over things as I actually understand whats happening under the hood :-)
There may well be a better solution than this one and if there is I would really appreciate hearing your thoughts - this is just the solution I came up with that so far fits my needs for this project (although this is experimental at the stage of posting).
use this:
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();
}
took from: Managing relationShips
Related
Not really sure why I'm not getting the child object populated.
My tables:
Product:
[ProductId]
,[Brand]
,[Model]
StoreProduct:
[StoreId]
,[ProductId]
,[StoreProductId]
Class
public class Product
{
public Guid ProductId { get; set; }
public string Brand { get; set; }
public string Model { get; set; }
public virtual List<StoreProduct> StoreProducts { get; set; }
}
public class StoreProduct
{
public int StoreId { get; set; } //Key 0
public Guid ProductId { get; set; } //Key 1
public Store Store { get; set; }
public Product Product { get; set; }
public string StoreProductId { get; set; } //A new Id specific for each store
}
My Dapper Code
string sql = "SELECT * FROM StoreProduct AS A INNER JOIN Product AS B ON A.ProductId = B.ProductId WHERE A.StoreProductId = #StoreProductId and A.StoreId = #StoreId";
var connection = AppDbContext.Database.GetDbConnection();
return connection.Query<StoreProduct, Product, Product>(
sql,
(StoreProduct, Product) => { StoreProduct.ProductId = Product.ProductId; return Product; },
new { StoreProductId = storeProductId, StoreId = StoreID }, splitOn: "ProductId")
.FirstOrDefault();
What the DB returns:
But... StoreProducts List is null.
Use Dapper the way it works.
var listProduct = new Dictionary<string, Product>();
var listStoreProduct = new Dictionary<string, StoreProduct>();
using var connection = _connectionProvider.GetConnection();
var query = "SELECT * FROM StoreProduct AS A INNER JOIN Product AS B ON A.ProductId = B.ProductId WHERE A.StoreProductId = #StoreProductId and A.StoreId = #StoreId";
var result = connection.Query<Product, StoreProduct, Product>(query, (product, storeProduct) =>
{
if (!listProduct.TryGetValue(product.Id, out var entry))
{
entry = product;
listProduct.Add(entry.Id, entry);
}
if (storeProduct != null && !listStoreProduct.TryGetValue(storeProduct.Id, out var procInstance))
{
listStoreProduct.Add(procInstance.Id, procInstance);
entry.ProcessInstance.Add(procInstance);
}
return entry;
}, splitOn: "ProductId").Distinct().ToList();
I hope I could have helped you.
I would like to use a base entity common to all entities as every table should have ad ID, InsertDate and LastModifiedDate.
As per documentation I should create a BaseEntity abstract class and every entity should inheritance from this.
public abstract class BaseEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public DateTime? InsertDateTime { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime? LastModifiedDateTime { get; set; }
}
Everything was ok until I started adding relations. Now after relations with foreign keys has been added the Migration is creating only one big table called BaseEntity with Discriminators but the abstract base entity should only be used to inheritance common properties.
I have read here that there are 3 type of inheritance but in EF Core 3.0 only TPH is available. Seeing examples on the web with abstract base class aren't having this problems.
I was wondering If I am missing something in my implementation if you guys please help me find out.
This:
modelBuilder.Entity<BaseEntity>()
declares BaseEntity as a database entity. Instead configure all the subtypes. Since they are mapped to seperate tables, they need seperate configuration. EG
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Ignore<BaseEntity>();
foreach (var et in modelBuilder.Model.GetEntityTypes())
{
if (et.ClrType.IsSubclassOf(typeof(BaseEntity)))
{
et.FindProperty("InsertDateTime").SetDefaultValueSql("getdate()");
et.FindProperty("InsertDateTime").ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAdd;
et.FindProperty("LastModifiedDateTime").SetDefaultValueSql("getdate()");
et.FindProperty("LastModifiedDateTime").ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAddOrUpdate;
}
}
base.OnModelCreating(modelBuilder);
}
Note, that this won't cause the LastModifiedDateTime to be updated for every change. That would require a trigger or an interceptor of some kind in EF.
Here's another alternative.
public abstract class BaseEntityWithUpdatedAndRowVersion
{
[Display(Name = "Updated By", Description = "User who last updated this meeting.")]
[Editable(false)]
[ScaffoldColumn(false)]
public string UpdatedBy { get; set; }
[Display(Name = "Updated", Description = "Date and time this row was last updated.")]
[Editable(false)]
[ScaffoldColumn(false)]
public DateTimeOffset UpdatedDateTime { get; set; }
[Display(Name = "SQL Server Timestamp", ShortName = "RowVersion", Description = "Internal SQL Server row version stamp.")]
[Timestamp]
[Editable(false)]
[ScaffoldColumn(false)]
public byte[] RowVersion { get; set; }
}
}
And its abstract configuration:
internal abstract class BaseEntityWithUpdatedAndRowVersionConfiguration <TBase> : IEntityTypeConfiguration<TBase>
where TBase: BaseEntityWithUpdatedAndRowVersion
{
public virtual void Configure(EntityTypeBuilder<TBase> entity)
{
entity.Property(e => e.UpdatedBy)
.IsRequired()
.HasMaxLength(256);
entity.Property(e => e.UpdatedDateTime)
.HasColumnType("datetimeoffset(0)")
.HasDefaultValueSql("(sysdatetimeoffset())");
entity.Property(e => e.RowVersion)
.IsRequired()
.IsRowVersion();
}
}
Here's a concrete class using the base entity.
public partial class Invitation: BaseEntityWithUpdatedAndRowVersion, IValidatableObject
{
[Display(Name = "Paper", Description = "Paper being invited.")]
[Required]
public int PaperId { get; set; }
[Display(Name = "Full Registration Fee Waived?", ShortName = "Fee Waived?",
Description = "Is the registration fee completely waived for this author?")]
[Required]
public bool IsRegistrationFeeFullyWaived { get; set; }
}
And its configuration code, calling the base configuration:
internal class InvitationConfiguration : BaseEntityWithUpdatedAndRowVersionConfiguration<Invitation>
{
public override void Configure(EntityTypeBuilder<Invitation> entity)
{
base.Configure(entity);
entity.HasKey(e => e.PaperId);
entity.ToTable("Invitations", "Offerings");
entity.Property(e => e.PaperId).ValueGeneratedNever();
entity.HasOne(d => d.Paper)
.WithOne(p => p.Invitation)
.HasForeignKey<Invitation>(d => d.PaperId)
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("Invitations_FK_IsFor_Paper");
}
}
Finally, this addition to the db context handles the updated by and updated date/time.
public partial class ConferenceDbContext : IdentityDbContext<ConferenceUser, ConferenceRole, int>
{
public override int SaveChanges()
{
AssignUpdatedByAndTime();
return base.SaveChanges();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
AssignUpdatedByAndTime();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
AssignUpdatedByAndTime();
return await base.SaveChangesAsync(cancellationToken).ConfigureAwait(true);
}
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
AssignUpdatedByAndTime();
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(true);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "App is not being globalized.")]
private void AssignUpdatedByAndTime()
{
//Get changed entities (added or modified).
ChangeTracker.DetectChanges();
var changedEntities = ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)
.ToList();
//Assign UpdatedDateTime and UpdatedBy properties if they exist.
AssignUpdatedByUserAndTime(changedEntities);
}
#region AssignUpdated
/// <summary>Assign updated-by & updated-when to any entity containing those attributes, including general category parent entities.</summary>
private void AssignUpdatedByUserAndTime(List<EntityEntry> changedEntities)
{
foreach (EntityEntry entityEntry in changedEntities)
{
//Some subcategory entities have the updated date/by attributes in the parent entity.
EntityEntry parentEntry = null;
string entityTypeName = entityEntry.Metadata.Name; //Full class name, e.g., ConferenceEF.Models.Meeting
entityTypeName = entityTypeName.Split('.').Last();
switch (entityTypeName)
{
case "Paper":
case "FlashPresentation":
case "Break":
parentEntry = entityEntry.Reference(nameof(SessionItem)).TargetEntry;
break;
default:
break;
}
AssignUpdatedByUserAndTime(parentEntry ?? entityEntry);
}
}
private void AssignUpdatedByUserAndTime(EntityEntry entityEntry)
{
if (entityEntry.Entity is BaseEntityWithUpdatedAndRowVersion
|| entityEntry.Entity is PaperRating)
{
PropertyEntry updatedDateTime = entityEntry.Property("UpdatedDateTime");
DateTimeOffset? currentValue = (DateTimeOffset?)updatedDateTime.CurrentValue;
//Avoid possible loops by only updating time when it has changed by at least 1 minute.
//Is this necessary?
if (!currentValue.HasValue || currentValue < DateTimeOffset.Now.AddMinutes(-1))
updatedDateTime.CurrentValue = DateTimeOffset.Now;
if (entityEntry.Properties.Any(p => p.Metadata.Name == "UpdatedBy"))
{
PropertyEntry updatedBy = entityEntry.Property("UpdatedBy");
string newValue = CurrentUserName; //ClaimsPrincipal.Current?.Identity?.Name;
if (newValue == null && !updatedBy.Metadata.IsColumnNullable())
newValue = string.Empty;
if (updatedBy.CurrentValue?.ToString() != newValue)
updatedBy.CurrentValue = newValue;
}
}
}
#endregion AssignUpdated
}
I have the following entities:
[Table("Customer", Schema = "dbo")]
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}
[Table("Payment", Schema = "dbo")]
public class Payment
{
public int PaymentId { get; set; }
public int CustomerId { get; set; }
public DateTime Period { get; set; }
public int Price { get; set; }
[ForeignKey("CustomerId")]
public Customer Customer { get; set; }
}
Now I want to filter Payment table by Period and Price. Each of predicate must be in its own Where method. So, I get the following:
int price = 200;
var period = new DateTime(2020, 10, 3);
using var db = new TestContext();
// Option 1: anonymous lambda
var payments1 = db.Payments
.Where(p => p.Period < period)
.Where(p => p.Price <= price);
foreach (var payment in payments1)
{
listBox.Items.Add(
$"Payment: Payment Id={payment.PaymentId}, " +
$"Customer Id`={payment.CustomerId}, " +
$"Period={payment.Period.ToShortDateString()}, " +
$"Price={payment.Price}");
}
EF6 generates correct SQL:
exec sp_executesql N'SELECT
[Extent1].[PaymentId] AS [PaymentId],
[Extent1].[CustomerId] AS [CustomerId],
[Extent1].[Period] AS [Period],
[Extent1].[Price] AS [Price]
FROM [dbo].[Payment] AS [Extent1]
WHERE ([Extent1].[Period] < #p__linq__0) AND ([Extent1].[Price] <= #p__linq__1)',
N'#p__linq__0 datetime2(7),#p__linq__1 int',
#p__linq__0='2020-10-03 00:00:00',
#p__linq__1=200
However, if I use Func lambda with the same condition:
// Option 2: Func<T, T> lambda
Func<Payment, bool> func = p => p.Period < period;
var payments2 = db.Payments.Where(func).Where(p => p.Price <= price);
I don't get the same SQL, but get this one:
SELECT
[Extent1].[PaymentId] AS [PaymentId],
[Extent1].[CustomerId] AS [CustomerId],
[Extent1].[Period] AS [Period],
[Extent1].[Price] AS [Price]
FROM [dbo].[Payment] AS [Extent1]
As far as I understand, EF switched to client-side evaluation. I wonder, why this happened? I'm using same lambda for filtering!
You need to use an Expression, not just a Func so EF can work out the names of the properties etc.
Try:
Expression<Func<Payment, bool>> func = p => p.Period < period;
I want to load 3 different models from a SQL Server stored procedure which returns 3 different tables, like:
select A.id, A.Name from tableA A
select B.id, B.Age from tableB B
select C.Test, C.Param from tableC C
Usually, I would handle a single result stored procedure with Entity Framework Core like this:
Context:
public virtual DbQuery<StoredProcedureModel> spModel{ get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Query<StoredProcedureModel>(entity =>
{
entity.Property(e => e.Id)
.HasColumnName("Id");
entity.Property(e => e.Name)
.HasColumnName("Name");
}
}
Repository:
return _context.StoredProcedureModel
.FromSql<StoredProcedureModel>(
"GET_ID_NAME #ID,#NAME",
new SqlParameter("#ID", ID),
new SqlParameter("#NAME", Name))
.ToList();
This is just a dummy example, but I wanted to know if there's a way to load all 3 tables into 3 different models (some of the tables returned have the same column names like "id").
Currently it is not possible with Entitiy-Framework but there is a FeatureRequest.
As a workaround, according to this blog post there is a solution with ADO.Net:
First code your classes:
public class TableA
{
public int Id { get; set; }
public string Name { get; set; }
}
public class TableB
{
public int Id { get; set; }
public int Age { get; set; }
}
public class TableC
{
public string Test { get; set; }
public string Param { get; set; }
}
public class StoredProcedureResult
{
public List<TableA> TableAEntries { get; set; }
public List<TableB> TableBEntries { get; set; }
public List<TableC> TableCEntries { get; set; }
}
Afterwards code the following function into your CustomDbContext class:
public async Task<StoredProcedureResult> GetStoredProcedureResult(int id, string name)
{
var connection = Database.GetDbConnection();
await connection.OpenAsync();
var command = connection.CreateCommand();
command.CommandText = "GET_ID_NAME #ID, #NAME";
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add(new Microsoft.Data.SqlClient.SqlParameter("#ID", id));
command.Parameters.Add(new Microsoft.Data.SqlClient.SqlParameter("#NAME", name));
var reader = await command.ExecuteReaderAsync();
var tableAEntries = new List<TableA>();
var tableBEntries = new List<TableB>();
var tableCEntries = new List<TableC>();
while (await reader.ReadAsync())
{
tableAEntries.Add(new TableA
{
Id = reader.GetInt32("id"),
Name = reader.GetString("name"),
});
}
await reader.NextResultAsync();
while (await reader.ReadAsync())
{
tableBEntries.Add(new TableB
{
Id = reader.GetInt32("id"),
Age = reader.GetInt32("Age"),
});
}
await reader.NextResultAsync();
while (await reader.ReadAsync())
{
tableCEntries.Add(new TableC
{
Test = reader.GetString("Test"),
Param = reader.GetString("Param"),
});
}
var storedProcedureResult = new StoredProcedureResult();
storedProcedureResult.TableAEntries = tableAEntries;
storedProcedureResult.TableBEntries = tableBEntries;
storedProcedureResult.TableCEntries = tableCEntries;
await reader.CloseAsync();
return storedProcedureResult;
}
I am new to the Dapper micro ORM. So far I am able to use it for simple ORM related stuff but I am not able to map the database column names with the class properties.
For example, I have the following database table:
Table Name: Person
person_id int
first_name varchar(50)
last_name varchar(50)
and I have a class called Person:
public class Person
{
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Please note that my column names in the table are different from the property name of the class to which I am trying to map the data which I got from the query result.
var sql = #"select top 1 PersonId,FirstName,LastName from Person";
using (var conn = ConnectionFactory.GetConnection())
{
var person = conn.Query<Person>(sql).ToList();
return person;
}
The above code won't work as the column names don't match the object's (Person) properties. In this scenario, is there anything i can do in Dapper to manually map (e.g person_id => PersonId) the column names with object properties?
Dapper now supports custom column to property mappers. It does so through the ITypeMap interface. A CustomPropertyTypeMap class is provided by Dapper that can do most of this work. For example:
Dapper.SqlMapper.SetTypeMap(
typeof(TModel),
new CustomPropertyTypeMap(
typeof(TModel),
(type, columnName) =>
type.GetProperties().FirstOrDefault(prop =>
prop.GetCustomAttributes(false)
.OfType<ColumnAttribute>()
.Any(attr => attr.Name == columnName))));
And the model:
public class TModel {
[Column(Name="my_property")]
public int MyProperty { get; set; }
}
It's important to note that the implementation of CustomPropertyTypeMap requires that the attribute exist and match one of the column names or the property won't be mapped. The DefaultTypeMap class provides the standard functionality and can be leveraged to change this behavior:
public class FallbackTypeMapper : SqlMapper.ITypeMap
{
private readonly IEnumerable<SqlMapper.ITypeMap> _mappers;
public FallbackTypeMapper(IEnumerable<SqlMapper.ITypeMap> mappers)
{
_mappers = mappers;
}
public SqlMapper.IMemberMap GetMember(string columnName)
{
foreach (var mapper in _mappers)
{
try
{
var result = mapper.GetMember(columnName);
if (result != null)
{
return result;
}
}
catch (NotImplementedException nix)
{
// the CustomPropertyTypeMap only supports a no-args
// constructor and throws a not implemented exception.
// to work around that, catch and ignore.
}
}
return null;
}
// implement other interface methods similarly
// required sometime after version 1.13 of dapper
public ConstructorInfo FindExplicitConstructor()
{
return _mappers
.Select(mapper => mapper.FindExplicitConstructor())
.FirstOrDefault(result => result != null);
}
}
And with that in place, it becomes easy to create a custom type mapper that will automatically use the attributes if they're present but will otherwise fall back to standard behavior:
public class ColumnAttributeTypeMapper<T> : FallbackTypeMapper
{
public ColumnAttributeTypeMapper()
: base(new SqlMapper.ITypeMap[]
{
new CustomPropertyTypeMap(
typeof(T),
(type, columnName) =>
type.GetProperties().FirstOrDefault(prop =>
prop.GetCustomAttributes(false)
.OfType<ColumnAttribute>()
.Any(attr => attr.Name == columnName)
)
),
new DefaultTypeMap(typeof(T))
})
{
}
}
That means we can now easily support types that require map using attributes:
Dapper.SqlMapper.SetTypeMap(
typeof(MyModel),
new ColumnAttributeTypeMapper<MyModel>());
Here's a Gist to the full source code.
This works fine:
var sql = #"select top 1 person_id PersonId, first_name FirstName, last_name LastName from Person";
using (var conn = ConnectionFactory.GetConnection())
{
var person = conn.Query<Person>(sql).ToList();
return person;
}
Dapper has no facility that allows you to specify a Column Attribute, I am not against adding support for it, providing we do not pull in the dependency.
For some time, the following should work:
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
I do the following using dynamic and LINQ:
var sql = #"select top 1 person_id, first_name, last_name from Person";
using (var conn = ConnectionFactory.GetConnection())
{
List<Person> person = conn.Query<dynamic>(sql)
.Select(item => new Person()
{
PersonId = item.person_id,
FirstName = item.first_name,
LastName = item.last_name
}
.ToList();
return person;
}
Here is a simple solution that doesn't require attributes allowing you to keep infrastructure code out of your POCOs.
This is a class to deal with the mappings. A dictionary would work if you mapped all the columns, but this class allows you to specify just the differences. In addition, it includes reverse maps so you can get the field from the column and the column from the field, which can be useful when doing things such as generating sql statements.
public class ColumnMap
{
private readonly Dictionary<string, string> forward = new Dictionary<string, string>();
private readonly Dictionary<string, string> reverse = new Dictionary<string, string>();
public void Add(string t1, string t2)
{
forward.Add(t1, t2);
reverse.Add(t2, t1);
}
public string this[string index]
{
get
{
// Check for a custom column map.
if (forward.ContainsKey(index))
return forward[index];
if (reverse.ContainsKey(index))
return reverse[index];
// If no custom mapping exists, return the value passed in.
return index;
}
}
}
Setup the ColumnMap object and tell Dapper to use the mapping.
var columnMap = new ColumnMap();
columnMap.Add("Field1", "Column1");
columnMap.Add("Field2", "Column2");
columnMap.Add("Field3", "Column3");
SqlMapper.SetTypeMap(typeof (MyClass), new CustomPropertyTypeMap(typeof (MyClass), (type, columnName) => type.GetProperty(columnMap[columnName])));
An easy way to achieve this is to just use aliases on the columns in your query.
If your database column is PERSON_ID and your object's property is ID, you can just do
select PERSON_ID as Id ...
in your query and Dapper will pick it up as expected.
Taken from the Dapper Tests which is currently on Dapper 1.42.
// custom mapping
var map = new CustomPropertyTypeMap(typeof(TypeWithMapping),
(type, columnName) => type.GetProperties().FirstOrDefault(prop => GetDescriptionFromAttribute(prop) == columnName));
Dapper.SqlMapper.SetTypeMap(typeof(TypeWithMapping), map);
Helper class to get name off the Description attribute (I personally have used Column like #kalebs example)
static string GetDescriptionFromAttribute(MemberInfo member)
{
if (member == null) return null;
var attrib = (DescriptionAttribute)Attribute.GetCustomAttribute(member, typeof(DescriptionAttribute), false);
return attrib == null ? null : attrib.Description;
}
Class
public class TypeWithMapping
{
[Description("B")]
public string A { get; set; }
[Description("A")]
public string B { get; set; }
}
Before you open the connection to your database, execute this piece of code for each of your poco classes:
// Section
SqlMapper.SetTypeMap(typeof(Section), new CustomPropertyTypeMap(
typeof(Section), (type, columnName) => type.GetProperties().FirstOrDefault(prop =>
prop.GetCustomAttributes(false).OfType<ColumnAttribute>().Any(attr => attr.Name == columnName))));
Then add the data annotations to your poco classes like this:
public class Section
{
[Column("db_column_name1")] // Side note: if you create aliases, then they would match this.
public int Id { get; set; }
[Column("db_column_name2")]
public string Title { get; set; }
}
After that, you are all set. Just make a query call, something like:
using (var sqlConnection = new SqlConnection("your_connection_string"))
{
var sqlStatement = "SELECT " +
"db_column_name1, " +
"db_column_name2 " +
"FROM your_table";
return sqlConnection.Query<Section>(sqlStatement).AsList();
}
Messing with mapping is borderline moving into real ORM land. Instead of fighting with it and keeping Dapper in its true simple (fast) form, just modify your SQL slightly like so:
var sql = #"select top 1 person_id as PersonId,FirstName,LastName from Person";
If you're using .NET 4.5.1 or higher checkout Dapper.FluentColumnMapping for mapping the LINQ style. It lets you fully separate the db mapping from your model (no need for annotations)
This is piggy backing off of other answers. It's just a thought I had for managing the query strings.
Person.cs
public class Person
{
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public static string Select()
{
return $"select top 1 person_id {nameof(PersonId)}, first_name {nameof(FirstName)}, last_name {nameof(LastName)}from Person";
}
}
API Method
using (var conn = ConnectionFactory.GetConnection())
{
var person = conn.Query<Person>(Person.Select()).ToList();
return person;
}
The simple solution to the problem Kaleb is trying to solve is just to accept the property name if the column attribute doesn't exist:
Dapper.SqlMapper.SetTypeMap(
typeof(T),
new Dapper.CustomPropertyTypeMap(
typeof(T),
(type, columnName) =>
type.GetProperties().FirstOrDefault(prop =>
prop.GetCustomAttributes(false)
.OfType<ColumnAttribute>()
.Any(attr => attr.Name == columnName) || prop.Name == columnName)));
The easier way (same as #Matt M's answer but corrected and added fallback to default map)
// override TypeMapProvider to return custom map for every requested type
Dapper.SqlMapper.TypeMapProvider = type =>
{
// create fallback default type map
var fallback = new DefaultTypeMap(type);
return new CustomPropertyTypeMap(type, (t, column) =>
{
var property = t.GetProperties().FirstOrDefault(prop =>
prop.GetCustomAttributes(typeof(ColumnAttribute))
.Cast<ColumnAttribute>()
.Any(attr => attr.Name == column));
// if no property matched - fall back to default type map
if (property == null)
{
property = fallback.GetMember(column)?.Property;
}
return property;
});
};
for all of you who use Dapper 1.12, Here's what you need to do to get this done:
Add a new column attribute class:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property]
public class ColumnAttribute : Attribute
{
public string Name { get; set; }
public ColumnAttribute(string name)
{
this.Name = name;
}
}
Search for this line:
map = new DefaultTypeMap(type);
and comment it out.
Write this instead:
map = new CustomPropertyTypeMap(type, (t, columnName) =>
{
PropertyInfo pi = t.GetProperties().FirstOrDefault(prop =>
prop.GetCustomAttributes(false)
.OfType<ColumnAttribute>()
.Any(attr => attr.Name == columnName));
return pi != null ? pi : t.GetProperties().FirstOrDefault(prop => prop.Name == columnName);
});
I know this is a relatively old thread, but I thought I'd throw what I did out there.
I wanted attribute-mapping to work globally. Either you match the property name (aka default) or you match a column attribute on the class property. I also didn't want to have to set this up for every single class I was mapping to. As such, I created a DapperStart class that I invoke on app start:
public static class DapperStart
{
public static void Bootstrap()
{
Dapper.SqlMapper.TypeMapProvider = type =>
{
return new CustomPropertyTypeMap(typeof(CreateChatRequestResponse),
(t, columnName) => t.GetProperties().FirstOrDefault(prop =>
{
return prop.Name == columnName || prop.GetCustomAttributes(false).OfType<ColumnAttribute>()
.Any(attr => attr.Name == columnName);
}
));
};
}
}
Pretty simple. Not sure what issues I'll run into yet as I just wrote this, but it works.
Kaleb Pederson's solution worked for me. I updated the ColumnAttributeTypeMapper to allow a custom attribute (had requirement for two different mappings on same domain object) and updated properties to allow private setters in cases where a field needed to be derived and the types differed.
public class ColumnAttributeTypeMapper<T,A> : FallbackTypeMapper where A : ColumnAttribute
{
public ColumnAttributeTypeMapper()
: base(new SqlMapper.ITypeMap[]
{
new CustomPropertyTypeMap(
typeof(T),
(type, columnName) =>
type.GetProperties( BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(prop =>
prop.GetCustomAttributes(true)
.OfType<A>()
.Any(attr => attr.Name == columnName)
)
),
new DefaultTypeMap(typeof(T))
})
{
//
}
}