I'm using multiple mapping for a current query and now I need to map another object on the initial query.
For example:
public class Part {
public int Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address {
public int Id { get; set; }
public string Street { get; set; }
public SiteOu Ou { get; set; }
}
public class SiteOu
public int Id { get; set; }
public string Name { get; set; }
}
Dapper:
connection.Query<Part, Address, Part>(sql, (part, address) => {
part.Address = address;
});
How do I get the Address class to have the SiteOu information?
This example isn't what I'm actually doing because I've actually got
Query<T1,T2,T3,T4,T5,TResult>();
I'm doing 1 select and 5 joins in my query. So hopefully I don't need more overloads of Query.
Dapper allows you to map a single row to multiple objects, so you can just map SiteOu as part of the same query.
[Test]
public void TestSplitOn()
{
var conn = new SqlConnection(#"Data Source=.\SQLEXPRESS;Integrated Security=true;Initial Catalog=db");
conn.Open();
const string sql = "select Id = 1, Name = 'My Part', " +
"Id = 2, Street = 'My Street', " +
"Id = 3, Name = 'My Site'";
var result = conn.Query<Part, Address, SiteOu, Part>(sql, (part, address, siteOu) =>
{
part.Address = address;
address.Ou = siteOu;
return part;
},
commandType: CommandType.Text
).FirstOrDefault();
Assert.That(result, Is.Not.Null);
Assert.That(result.Address, Is.Not.Null);
Assert.That(result.Address.Ou, Is.Not.Null);
}
Important Note: Dapper assumes your Id columns are named "Id" or "id", if your primary key is different or you would like to split the wide row at point other than "Id", use the optional 'splitOn' parameter.
If you have more that 5 types to map, another out of the box option is to use QueryMultiple extension. Here is an example from the Dapper docs.
var sql =
#"
select * from Customers where CustomerId = #id
select * from Orders where CustomerId = #id
select * from Returns where CustomerId = #id";
using (var multi = connection.QueryMultiple(sql, new {id=selectedId}))
{
var customer = multi.Read<Customer>().Single();
var orders = multi.Read<Order>().ToList();
var returns = multi.Read<Return>().ToList();
...
}
Also check out this thread.
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 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;
}
How do you use Dapper's Multi-Mapping feature on two fields using the same table? i.e ClientInfo has two Address objects.
public class ClientInfo
{
public Guid Id => Guid.NewGuid();
public string FirstName { get; set; }
public string LastName { get; set; }
public Address PostalAddress { get; set; }
public Address BillingAddress { get; set; }
public int ContactNumber { get; set; }
}
public class Address
{
public Guid Id = Guid.NewGuid();
public string FirstLine { get; set; }
public string SecondLine { get; set; }
public string Town { get; set; }
public string PostCode { get; set; }
}
Tables
Relational - Address.Id used in ClientInfo.PostalAddress / BillingAddress
tbl.Address
|Id|FirstLine|SecondLine|Town|PostCode
tbl.ClientInfo
|Id|FirstName|LastName|PostalAddress|BillingAddress|etc..
Current implementation
Results only in all but PostalAddress being mapped.
var sql = #"select * from ClientInfo c left join Address as a on a.Id = c.PostalAddress left join Address as ad on ad.Id = c.BillingAddress";
var clients = connection.Query<ClientInfo, Address, Address, ClientInfo>(
sql,
(client, postal, billing) =>
{
client.PostalAddress = postal;
client.BillingAddress = billing;
return client;
},
splitOn: "PostalAddress,BillingAddress")
.Distinct()
.ToList();
return clients;
The splitOn parameter tells Dapper when/where to start mapping the next object, so you need to ensure that your SQL query returns the information in the correct order. Right now, you return 2 guids for PostalAddress and BillingAddress. Dapper doesn't know how to map them both.
select * from ... join ... will result in the Address data ordered AFTER the ClientInfo.PostalAddress and ClientInfo.BillingAddress columns.
Try: SELECT c.Id, c.FirstName, c.LastName, c.ContactNumber, a.*, ad.* FROM ClientInfo c LEFT JOIN Address AS a ON a.Id = c.PostalAddress JOIN Address AS ad ON ad.Id = c.BillingAddress
As you can see, removing the * effectively excludes the PostalAddress and BillingAddress guids from the results and we can now now splitOn: "Id,Id".
You will of course not have to provide the GUIDs in the select statement, this is just for the test to work.
[Test]
public void TestAbcd()
{
using (var dbConnection = new SqlConnection(_connectionString))
{
const string sql = #"WITH ClientInfo AS (
SELECT * FROM (
VALUES (#ci1, #adr1, #adr2), (#ci1, #adr3, #adr4)
) AS a (Id, PostalAddress, BillingAddress)
),
Address AS (
SELECT * FROM (
VALUES
(#adr1), (#adr2), (#adr3), (#adr4)
) AS a (Id)
)
select * from ClientInfo c left join Address as a on a.Id = c.PostalAddress left join Address as ad on ad.Id = c.BillingAddress";
dbConnection.Open();
var clients = dbConnection.Query<ClientInfo, Address, Address, ClientInfo>(
sql,
(client, postal, billing) =>
{
client.PostalAddress = postal;
client.BillingAddress = billing;
return client;
},
splitOn: "PostalAddress,BillingAddress", param: new {
ci1 = Guid.NewGuid(),
ci2 = Guid.NewGuid(),
adr1 = Guid.NewGuid(),
adr2 = Guid.NewGuid(),
adr3 = Guid.NewGuid(),
adr4 = Guid.NewGuid()
})
.Distinct()
.ToList();
}
}
From Github:
Dapper allows you to map a single row to multiple objects. This is a
key feature if you want to avoid extraneous querying and eager load
associations.
Example:
Consider 2 classes: Post and User
> class Post {
> public int Id { get; set; }
> public string Title { get; set; }
> public string Content { get; set; }
> public User Owner { get; set; } }
>
> class User {
> public int Id { get; set; }
> public string Name { get; set; } }
Now let us say that we want to map a query that joins both the posts
and the users table. Until now
if we needed to combine the result of 2 queries, we'd need a new
object to express it but it makes more sense in this case to put the
User object inside the Post object.
When I do this (My classes are different names, but same construct), I get a Post and a User, a Post and a User. I'm using the Web API, so this is all JSON, if that matters. This is the way I'd see it if I did straight SQL in the Management Studio, you get the many rows and the corresponding User records
What if I want to send back the JSON that has the User once and all the posts in an array, then the next User, array of posts, etc.
id title content id name
1 Article1 Content1 55 Smith
2 Article2 Content2 55 Smith
3 Article3 Content3 55 Smith
I get the JSON back that has the User information over and over (as expected but not wanted). It's backwards.
What I want is a JSON object that has a format like this (I think this is correct):
{
"User": 55,
"Name": "Smith",
"Post": [
{
"id": 1,
"title": "title1",
"content":"MyContent1"
},
{
"id": 2,
"title": "title2",
"content":"MyContent2"
},
{
"id": 3,
"title": "title3",
"content":"MyContent2"
}
]
}
How do I do this? Right now I get the reverse. I thought I would simply change the classes around, but I did not because of the instructions on Github, the "makes more sense" part. I am using this,
(List<Post>)db.Query<Post, User, Paper>(sqlString, (post, user) => { post.user = user; return post; }, splitOn: "id");
I know I don't need the splitOn here, but in my real query the name is different than id.
This is pretty close:
https://www.tritac.com/developers-blog/dapper-net-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);
}
if (shop.Accounts == null)
shop.Accounts = new List<Account>();
shop.Accounts.Add(a);
return shop;
},
).AsQueryable();
var resultList = lookup.Values;
It makes the first object identifier. Not sure if I can use it like that or not. But this does do the array of books like I was asking, and I did not have to create a special object. Originally, it was supposed to be on Google Code, but I couldn't find this test on Github.
Since your SQL query is returning the flat records, i suggest you create a flat POCO and use dapper to map the result set to a collection of this. Once you have data in this collection, you can use LINQ GroupBy method to group it the way you want.
Assuming you have classes like
public class User
{
public int Id { set;get;}
public string Name { set;get;}
public IEnumerable<Post> Posts { set;get;}
}
public class Post
{
public int Id { set;get;}
public string Title{ set;get;}
public string Content { set;get;}
}
Now create the POCO for the flat result set row
public class UserPost
{
public int Id { set; get; }
public string Title { set; get; }
public string Content { set; get; }
public int UserId { set; get; }
public string Name { set; get; }
}
Now update your SQL query to return a result set with column name matching the above properties.
Now use Dapper to get the flat records
var userposts= new List<UserPost>();
using (var conn = new SqlConnection("YourConnectionString"))
{
userposts = conn.Query<UserPost>(query).ToList();
}
Now apply GroupBy
var groupedPosts = userposts.GroupBy(f => f.UserId, posts => posts, (k, v) =>
new User()
{
UserId = k,
Name = v.FirstOrDefault().Name,
Posts = v.Select(f => new Post() { Id = f.Id,
Title= f.Title,
Content = f.Content})
}).ToList();
Another option is to use .QueryMultiple
[Test]
public void TestQueryMultiple()
{
const string sql = #"select UserId = 55, Name = 'John Doe'
select PostId = 1, Content = 'hello'
union all select PostId = 2, Content = 'world'";
var multi = _sqlConnection.QueryMultiple(sql);
var user = multi.Read<User>().Single();
user.Posts = multi.Read<Post>().ToList();
Assert.That(user.Posts.Count, Is.EqualTo(2));
Assert.That(user.Posts.First().Content, Is.EqualTo("hello"));
Assert.That(user.Posts.Last().Content, Is.EqualTo("world"));
}
Update:
To return multiple users and their posts:
[Test]
public void TestQueryMultiple2()
{
const string sql = #"select UserId = 55, Name = 'John Doe'
select UserId = 55, PostId = 1, Content = 'hello'
union all select UserId = 55, PostId = 2, Content = 'world'";
var multi = _sqlConnection.QueryMultiple(sql);
var users = multi.Read<User>().ToList();
var posts = multi.Read<Post>().ToList();
foreach (var user in users)
{
user.Posts.AddRange(posts.Where(x => x.UserId == user.UserId));
}
Assert.That(users.Count, Is.EqualTo(1));
Assert.That(users.First().Posts.First().Content, Is.EqualTo("hello"));
Assert.That(users.First().Posts.Last().Content, Is.EqualTo("world"));
}
Actually I have a query which returns result containing column(for ex.Address) of type varchar but the domain model for that table containing property of type object(for ex. Address Address).Because of which it trows error which says could not cast string to Comment.I cant figure out how to resolve this issue with dapper .net.
Code snippet:
IEnumerable<Account> resultList = conn.Query<Account>(#"
SELECT *
FROM Account
WHERE shopId = #ShopId",
new { ShopId = shopId });
The Account object is for example.
public class Account {
public int? Id {get;set;}
public string Name {get;set;}
public Address Address {get;set;}
public string Country {get;set;}
public int ShopId {get; set;}
}
As there is type mismatch between database table column(Address) and domain model property(Address) dapper throws exception.So is there is any way to map that properties though dapper..
Another option is to use Dapper's Multi-Mapping feature.
public class TheAccount
{
public int? Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
public string Country { get; set; }
public int ShopId { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
}
public class Class1
{
[Test]
public void MultiMappingTest()
{
var conn =
new SqlConnection(
#"Data Source=.\SQLEXPRESS; Integrated Security=true; Initial Catalog=MyDb");
conn.Open();
const string sql = "select Id = 1, Name = 'John Doe', Country = 'USA', ShopId = 99, " +
" Street = '123 Elm Street', City = 'Gotham'";
var result = conn.Query<TheAccount, Address, TheAccount>(sql,
(account, address) =>
{
account.Address = address;
return account;
}, splitOn: "Street").First();
Assert.That(result.Address.Street, Is.Not.Null);
Assert.That(result.Country, Is.Not.Null);
Assert.That(result.Name, Is.Not.Null);
}
}
The only issue I see with this is that you'll have to list all of the Account fields, followed by the Address fields in your select statement, to allow splitOn to work.
Since there is a type mismatch between your POCO and your database, you'll need to provide a mapping between the two.
public class Account {
public int? Id {get;set;}
public string Name {get;set;}
public string DBAddress {get;set;}
public Address Address
{
// Propbably optimize here to only create it once.
get { return new Address(this.DBAddress); }
}
public string Country {get;set;}
public int ShopId {get; set;}
}
Something like that - you match the db column to the property DBAddress (You need to provide an alias like SELECT Address as DBAddress instead of *) and provide a get method on your Address object which creates / reuses a type of Address with the contents of the db value.