Unexpected behaviour with a multi-mapping query using Dapper.net - c#

I've only just started looking at Dapper.net and have just been experimenting with some different queries, one of which is producing weird results that i wouldn't expect.
I have 2 tables - Photos & PhotoCategories, of which are related on CategoryID
Photos Table
PhotoId (PK - int)
CategoryId (FK - smallint)
UserId (int)
PhotoCategories Table
CategoryId (PK - smallint)
CategoryName (nvarchar(50))
My 2 classes:
public class Photo
{
public int PhotoId { get; set; }
public short CategoryId { get; set; }
public int UserId { get; set; }
public PhotoCategory PhotoCategory { get; set; }
}
public class PhotoCategory
{
public short CategoryId { get; set; }
public string CategoryName { get; set; }
{
I want to use multi-mapping to return an instance of Photo, with a populated instance of the related PhotoCategory.
var sql = #"select p.*, c.* from Photos p inner
join PhotoCategories c
on p.CategoryID = c.CategoryID where p.PhotoID = #pid";
cn.Open();
var myPhoto = cn.Query<Photo, PhotoCategory, Photo>(sql,
(photo, photoCategory) => { photo.PhotoCategory = photoCategory;
return photo; },
new { pid = photoID }, null, true, splitOn: "CategoryID").Single();
When this is executed, not all of the properties are getting populated (despite the same names between the DB table and in my objects.
I noticed that if I don't 'select p.* etc.' in my SQL, and instead.
I explicitly state the fields.
I want to return EXCLUDING p.CategoryId from the query, then everything gets populated (except obviously the CategoryId against the Photo object which I've excluded from the select statement).
But i would expect to be able to include that field in the query, and have it, as well as all the other fields queried within the SQL, to get populated.
I could just exclude the CategoryId property from my Photo class, and always use Photo.PhotoCategory.CategoryId when i need the ID.
But in some cases I might not want to populate the PhotoCategory object when I get an instance of
the Photo object.
Does anyone know why the above behavior is happening? Is this normal for Dapper?

I just committed a fix for this:
class Foo1
{
public int Id;
public int BarId { get; set; }
}
class Bar1
{
public int BarId;
public string Name { get; set; }
}
public void TestMultiMapperIsNotConfusedWithUnorderedCols()
{
var result = connection.Query<Foo1,Bar1,
Tuple<Foo1,Bar1>>(
"select 1 as Id, 2 as BarId, 3 as BarId, 'a' as Name",
(f,b) => Tuple.Create(f,b), splitOn: "BarId")
.First();
result.Item1.Id.IsEqualTo(1);
result.Item1.BarId.IsEqualTo(2);
result.Item2.BarId.IsEqualTo(3);
result.Item2.Name.IsEqualTo("a");
}
The multi-mapper was getting confused if there was a field in the first type, that also happened to be in the second type ... AND ... was used as a split point.
To overcome now dapper allow for the Id field to show up anywhere in the first type. To illustrate.
Say we have:
classes: A{Id,FooId} B{FooId,Name}
splitOn: "FooId"
data: Id, FooId, FooId, Name
The old method of splitting was taking no account of the actual underlying type it was mapping. So ... it mapped Id => A and FooId, FooId, Name => B
The new method is aware of the props and fields in A. When it first encounters FooId in the stream it does not start a split, since it knows that A has a property called FooId which needs to be mapped, next time it sees FooId it will split, resulting in the expected results.

I'm having a similar problem. It's to do with the fact that both the child and the parent have the same name for the field that is being split on. The following for example works:
class Program
{
static void Main(string[] args)
{
var createSql = #"
create table #Users (UserId int, Name varchar(20))
create table #Posts (Id int, OwnerId int, Content varchar(20))
insert #Users values(99, 'Sam')
insert #Users values(2, 'I am')
insert #Posts values(1, 99, 'Sams Post1')
insert #Posts values(2, 99, 'Sams Post2')
insert #Posts values(3, null, 'no ones post')
";
var sql =
#"select * from #Posts p
left join #Users u on u.UserId = p.OwnerId
Order by p.Id";
using (var connection = new SqlConnection(#"CONNECTION STRING HERE"))
{
connection.Open();
connection.Execute(createSql);
var data = connection.Query<Post, User, Post>(sql, (post, user) => { post.Owner = user; return post; }, splitOn: "UserId");
var apost = data.First();
apost.Content = apost.Content;
connection.Execute("drop table #Users drop table #Posts");
}
}
}
class User
{
public int UserId { get; set; }
public string Name { get; set; }
}
class Post
{
public int Id { get; set; }
public int OwnerId { get; set; }
public User Owner { get; set; }
public string Content { get; set; }
}
But the following does not because "UserId" is used in both tables and both objects.
class Program
{
static void Main(string[] args)
{
var createSql = #"
create table #Users (UserId int, Name varchar(20))
create table #Posts (Id int, UserId int, Content varchar(20))
insert #Users values(99, 'Sam')
insert #Users values(2, 'I am')
insert #Posts values(1, 99, 'Sams Post1')
insert #Posts values(2, 99, 'Sams Post2')
insert #Posts values(3, null, 'no ones post')
";
var sql =
#"select * from #Posts p
left join #Users u on u.UserId = p.UserId
Order by p.Id";
using (var connection = new SqlConnection(#"CONNECTION STRING HERE"))
{
connection.Open();
connection.Execute(createSql);
var data = connection.Query<Post, User, Post>(sql, (post, user) => { post.Owner = user; return post; }, splitOn: "UserId");
var apost = data.First();
apost.Content = apost.Content;
connection.Execute("drop table #Users drop table #Posts");
}
}
}
class User
{
public int UserId { get; set; }
public string Name { get; set; }
}
class Post
{
public int Id { get; set; }
public int UserId { get; set; }
public User Owner { get; set; }
public string Content { get; set; }
}
Dapper's mapping seems to get very confused in this scenario. Think this describes the issue but is there a solution / workaround we can employ (OO design decisions aside)?

I know this question is old but thought I would save someone 2 minutes with the obvious answer to this: Just alias one id from one table:
ie:
SELECT
user.Name, user.Email, user.AddressId As id, address.*
FROM
User user
Join Address address
ON user.AddressId = address.AddressId

Related

Dapper does not properly map rows selected with LEFT JOIN (splitOn)

I have two models Vendors and Users. Each user may or may not have only one Vendor.
Vendors may have several Users.
Models:
public class AppUser
{
public int Id { get; set; }
public string? FullName { get; set; }
public string? Position { get; set; }
public int StatusId { get; set; }
public int VendorId { get; set; }
}
public class Vendor
{
public int VendorId { get; set; }
public string? VendorCode { get; set; }
public string? VendorName { get; set; }
public int StatusId { get; set; }
public List<AppUser> CompanyUsers { get; set; }
}
SQL Tables:
I need to select all Vendors with related Users (if such exists).
My query:
SELECT
v.VendorId
,v.VendorCode
,v.VendorName
,v.Status StatusId
,(CASE
WHEN v.Status = 0 THEN 'Draft'
WHEN v.Status = 1 THEN 'Open'
WHEN v.Status = 2 THEN 'Closed'
WHEN v.Status = 3 THEN 'Blacklisted'
END) StatusName
,au.Id
,au.FullName
,au.Position
,au.StatusId
,(CASE
WHEN au.StatusId = 0 THEN 'Draft'
WHEN au.StatusId = 1 THEN 'Open'
WHEN au.StatusId = 2 THEN 'Closed'
END) StatusName
FROM Procurement.Vendors v
LEFT JOIN Config.AppUser au
ON v.VendorId = au.VendorId
and the result:
Because only Vendor with Id 20 has users, it appears 3 times, which is expected behavior. Now I want to use Dapper's splitOn function to map all users under vendor 20. I split by user's Id column.
public async Task<IEnumerable<Vendor>?> GetAllVendors(int businessUnitId)
{
var currentUser = await _appUserService.GetCurrentUserAsync();
var p = new DynamicParameters();
p.Add("#UserId", currentUser.Id, DbType.Int32, ParameterDirection.Input);
p.Add("#BusinessUnitId", businessUnitId, DbType.Int32, ParameterDirection.Input);
using IDbConnection cn = new SqlConnection(_sqlDataAccess.ConnectionString);
return await cn.QueryAsync<Vendor, AppUser, Vendor>("dbo.SP_ZZZTest",
(vendor, user) =>
{
if (vendor.CompanyUsers == null && user != null) { vendor.CompanyUsers = new(); };
if (user != null) { vendor.CompanyUsers.Add(user); };
return vendor;
},
param: p,
splitOn: "Id",
commandType: CommandType.StoredProcedure);
}
And here is the result I get:
As a result, Dapper did not map Users under a single Vendor. But instead mapped each user as a List of users with a single item duplicating Vendor's data on 3 rows.
What did I wrong?
Yes, this is a common "problem". But it is really simple to solve once you understand the process behind the lambda call.
The lambda expression receives the three records created by the query and before calling the lambda, Dapper splits each record in two at the point of the splitOn configuration. In this process a new Vendor and new AppUser instance will be created for each row processed.
So the Vendor instance received at the second/third call is not the same instance of the first/second call. Dapper doesn't have this kind of processing logic (and I think it is right to avoid it from a performance point of view). So the code above, adds each AppUser to three different instances of Vendor.
It is up to you to 'discover' that the Vendor received contains the same data of a previous call. But it is easy to solve if there is some kind of unique key that identifies a Vendor (the PK of the record)
So this "problem" can be solved using a Dictionary where the key is the PK of the Vendor and you store each unique Vendor data passed by Dapper under that dictionary key. Then you could check if the Vendor data received is already in the Dictionary and use the dictionary instance to add the AppUser.
Dictionary<int, Vendor> result = new Dictionary<int, Vendor>();
.....
using IDbConnection cn = new SqlConnection(_sqlDataAccess.ConnectionString);
_ = await cn.QueryAsync<Vendor, AppUser, Vendor>("dbo.SP_ZZZTest",
(vendor, user) =>
{
if(!result.ContainsKey(vendor.vendorId))
{
vendor.CompanyUsers = new();
result.Add(vendor.vendorId, vendor);
}
Vendor current = result[vendor.vendorId];
if (user != null)
current.CompanyUsers.Add(user);
return vendor;
},
param: p,
splitOn: "Id",
commandType: CommandType.StoredProcedure);
// A VERY IMPORTANT POINT...
// You want to return the Vendors stored in the Values of the
// Dictionary, not the Vendors returned by the QueryAsync call
return result.Values;

Insert dependent Values with Fluent Migrator and EF Core HiLo

I have the following table:
public class Category
{
public int Id{ get; set; }
public string Name { get; set; }
public int? ParentId { get; set; }
public Category Parent { get; set; }
}
with the following mapping:
public class CategoryMap: IEntityTypeConfiguration<Category>
{
public void Configure(EntityTypeBuilder<Category> builder)
{
builder.ToTable(nameof(Category));
builder.Property(x => x.Id).UseHiLo();
//other properties
}
}
Now I want to use fluentmigrator to create two new Entries. But the entries are related. The second category has the first category as a parent. How can I achieve it?
I can't use fluentmigrator, because I don't have the Id
Insert
.IntoTable("Category")
.Row(new
{
Name = "Category1"
});
Insert
.IntoTable("Category")
.Row(new
{
Name = "Category2",
ParentId = 0 //Category1?
});
I could write a sql query, but I still don't have the Id. I could use a random Id, but it could end with a Problem in the HiLo algorithm.
Insert Into Category
(
[Id],
[Name],
[ParentId]
)values(
0, //?
'Category1',
null
)
I could use:
SELECT ##IDENTITY
SELECT IDENT_CURRENT('Category')
But I use the HiLo algorithm to create the Id's. It should be possible to get the next HiLo value somehow for my insert script and use it. Or is there a better way to achieve it?

How to call Stored Procedure with join on multiple tables in Entity Framework Core?

I have to call a stored procedure which is selecting records from multiple tables.
I have tried the following code, but it's returning null for columns from other tables than the entity class.
private async Task<IEnumerable<TEntity>> InvokeStoredProcedureAsync(string input = "")
{
var storedProcedureName = "sp_BulkSelect";
using (var db = new MyDbContext(_options))
{
var result = await db.Set<TEntity>().FromSql(storedProcedureName + " #inputIds", new SqlParameter("inputIds", input)).ToListAsync();
return result;
}
}
Stored procedure:
SELECT
[MainTable].[Id],
[Table1Id],
[Table2Id],
[MainTable].[Table1Code],
[Table2].[Table2Code]
FROM
[MainTable] [MainTable]
LEFT JOIN
[Table1] [Table1] ON [MainTable].Table1Id = [Table1].[Id]
LEFT JOIN
[Table2] [Table2] ON [MainTable].[Table2Id] = [Table2].[Id];
MainTable class:
[Table("MainTable")]
public class MainTable : FullAuditedEntity
{
[ForeignKey("Table1Id")]
public virtual Table1 Table1 { get; set; }
public virtual int Table1Id { get; set; }
[ForeignKey("Table2Id")]
public virtual Table2 Table2 { get; set; }
public virtual int? Table2Id { get; set; }
}
So when I call this stored procedure, Table1Code and Table2Code are missing in the return value.
I tried to add the following code in MainTable class, but its also not working.
[NotMapped]
public virtual string Table2Code { get; set; }
[NotMapped]
public virtual string Table1Code { get; set; }
Then I removed [NotMapped] from both the properties and added migration, in this case, its returning proper value. But It will add two columns in MainTable. It's really a BAD design.
So my question is how to select columns from multiple tables in the stored procedure in Entity Framework Core.
I'm using EF Core 2.0.
I think there has to be some way to call the stored procedure with using Entity and then map it to any class because select columns from multiple tables using join is a very basic requirement.
I tried the similar solution, but its giving compilation error.
'DatabaseFacade' does not contain a definition for 'SqlQuery' and no
extension method 'SqlQuery' accepting a first argument of type
'DatabaseFacade' could be found (are you missing a using directive or
an assembly reference?)
The complete idea to get data from a stored procedure is as follows:
You need to add an entity that has the same properties as the procedures select query has.
Add the entity to your DbContext and Create a migration. Change the code in the Up() and Down() methods of the migration so that it creates the procedure in the database.
Now use the FromSql() method to get the data a normal entity data.
Here is some code that can guide you. Suppose you have these entities in your application domain:
Student
Parent
SchoolClass
Section
Enrollment
Migrations up method
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "StudentDetails");
migrationBuilder.Sql(
#"create proc GetStudentDetail
#ssid int,
#sectionId int = null
as
select Id, name, Gender, RollNumber, Status, Type,
FatherName, FatherContact, SchoolClass, Section,
SsId, SectionId, EnrollmentId
from
(
SELECT stu.Id, stu.name, stu.Gender, en.RollNumber, en.Status, en.Type,
p.FatherName, p.FatherContact, sc.Name as SchoolClass, sec.Name as Section,
ss.SessionId as SsId, sec.Id as SectionId, en.Id as EnrollmentId,
en.EntryDate, row_number() over (partition by studentid order by en.entrydate desc) as rowno
from SchoolSessions ss
join SchoolClasses sc on ss.SessionId = sc.ssid
join Sections sec on sc.Id = sec.ClassId
join Enrollments en on sec.id = en.SectionId
join Students stu on en.StudentId = stu.Id
join parents p on stu.ParentId = p.Id
where ss.SessionId = #ssid
) A
where rowno = 1 and
(SectionId = #sectionId or #sectionId is null)"
);
}
Migrations down method
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("drop proc GetStudentDetail");
migrationBuilder.CreateTable(
name: "StudentDetails",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
EnrollmentId = table.Column<int>(nullable: false),
FatherContact = table.Column<string>(nullable: true),
FatherName = table.Column<string>(nullable: true),
Gender = table.Column<int>(nullable: false),
Name = table.Column<string>(nullable: true),
RollNumber = table.Column<string>(nullable: true),
SchoolClass = table.Column<string>(nullable: true),
Section = table.Column<string>(nullable: true),
SectionId = table.Column<int>(nullable: false),
SsId = table.Column<int>(nullable: false),
Status = table.Column<int>(nullable: false),
Type = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StudentDetails", x => x.Id);
});
}
The fake entity: All properties in this entity are coming from the above-said entities. You can call it a fake entity.
public class StudentDetail
{
public int Id { get; set; }
public string Name { get; set; }
public Gender Gender { get; set; }
public string RollNumber { get; set; }
public StudentStatus Status { get; set; }
public StudentType Type { get; set; }
public string FatherName { get; set; }
public string FatherContact { get; set; }
public string SchoolClass { get; set; }
public string Section { get; set; }
public int SsId { get; set; }
public int SectionId { get; set; }
public int EnrollmentId { get; set; }
}
Service layer to get data
public IEnumerable<StudentDetail> GetStudentDetails(int ssid)
{
var ssidParam = new SqlParameter("#ssid", ssid);
var result = _appDbContext.StudentDetails.FromSql("exec GetStudentDetail #ssid", ssidParam).AsNoTracking().ToList();
return result;
}
That's how it works in EF Core 2.1:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Query<YourModel>();
}
SqlParameter value1Input = new SqlParameter("#Param1", value1 ?? (object)DBNull.Value);
SqlParameter value2Input = new SqlParameter("#Param2", value2 ?? (object)DBNull.Value);
List<YourModel> result;
using (var db = new MyDbContext(_options))
{
result = await db.Query<YourModel>().FromSql("STORED_PROCEDURE #Param1, #Param2", value1Input, value2Input).ToListAsync();
}
Source
Below steps works in DB Design first approach EF core 3.1.0
1) Suppose SP(sp_BulkSelect) return 3 column by multiple tables(col1 int,col2 string,col3 string)
2) create fake Class which will hold this Data, DataType should be same as per SP column
public Class1
{
public int col1 {get;set;}
public string col2 {get;set;}
public string col3 {get;set;}
}
3) Used Modelbuilder to map call and entity in MyDbContext class
modelBuilder.Entity<Class1>(entity =>
{
entity.HasNoKey();
entity.Property(e => e.col1);
entity.Property(e => e.col2);
entity.Property(e => e.col3);
});
4) create fake table in MyDbContext class
public virtual DbSet<Class1> Class_1 { get; set; }
5) Used MyDbContext class to call SP
var Class1data = MyDbContext.Class_1.FromSqlRaw("EXECUTE sp_BulkSelect {0}", id);
1- Create a View from select part of sql query --without where condition--.
2- Then generate your model from database and Entity Framework generates a model for your view.
3- Now you can execute your stored procedure using generated model from view
dbContext.GeneratedView.FromSqlRaw("MyStoredProcedure {0}, {1} ", param1, param2)

Entity Framework - incorrectly doing 2 select statements instead of a join

I have a fairly simple (code first) model:
Employee
[Table("vEmployee")] //note v - it's a view
public class Employee
{
[Key]
public int EmployeeNumber { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
EmployeeHolidayEntitlement
[Table("tblEmployeeHolidayEntitlement")]
public class EmployeeHolidayEntitlement
{
[Key]
public int EmployeeNumber { get; set; }
public virtual Employee Employee { get; set; }
public decimal StandardEntitlement { get; set; }
//.....omitted for brevity
}
Note that EmployeeHolidayEntitlement is mapped to a table, and Employee is mapped to a view
When building my context, I do:
(not sure if this is correct!)
modelBuilder.Entity<Employee>()
.HasOptional(x => x.HolidayEntitlement)
.WithRequired(x => x.Employee);
Now, when I query, like this:
var db = new ApiContext();
var result = db.Employees.ToList();
It's very slow.
If I look in SQL profiler, I can see that instead of one statement (joining vEmployee and tblEmployeeHolidayEntitlement) I get many statements executed (one per Employee record) - for example:
First, it selects from vEmployee
SELECT
[Extent1].[id] AS [EmployeeNumber],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName],
FROM [dbo].[vEmployee] AS [Extent1]
then one of these for each record returned
exec sp_executesql N'SELECT
[Extent1].[EmployeeNumber] AS [EmployeeNumber],
[Extent1].[StandardEntitlement] AS [StandardEntitlement]
FROM [dbo].[tblEmployeeHolidayEntitlement] AS [Extent1]
WHERE [Extent1].[EmployeeNumber] = #EntityKeyValue1',N'#EntityKeyValue1 int',#EntityKeyValue1=175219
This doesn't seem right to me -
I would of thought it should be doing something more along the lines of a LEFT JOIN like
SELECT *
FROM [dbo].[vEmployee] employee
LEFT JOIN
[dbo].[tblEmployeeHolidayEntitlement employeeEntitlement
ON
employee.id = employeeEntitlement.employeenumber
You have to use the Include method, like db.Employees.Include(e => e.HolidayEntitlement).ToList(). If you don't and you access the property you'll trigger lazy loading. That's what's happening to you.
For more information check the documentation on loading. The short of it is that if it always joined your entire object graph it'd be unacceptably slow.

Dapper simple mapping

Table:
create table Documents
(Id int,
SomeText varchar(100),
CustomerId int,
CustomerName varchar(100)
)
insert into Documents (Id, SomeText, CustomerId, CustomerName)
select 1, '1', 1, 'Name1'
union all
select 2, '2', 2, 'Name2'
Classes:
public class Document
{
public int Id { get; set; }
public string SomeText { get; set; }
public Customer { get; set; }
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}
How can I get all Documents with their Customers with Dapper? This gives me all documents, but the customer is null (of course):
connection.Query<Document>("select Id, SomeText, CustomerId, CustomerName from Documents")...
EDIT - similar, but more advanced mapping question: Dapper intermediate mapping
Example taken from dapper project page (see the Multi Mapping section):
var sql =
#"select * from #Posts p
left join #Users u on u.Id = p.OwnerId
Order by p.Id";
var data = connection.Query<Post, User, Post>(sql, (post, user) => { post.Owner = user; return post;});
var post = data.First();
post.Content.IsEqualTo("Sams Post1");
post.Id.IsEqualTo(1);
post.Owner.Name.IsEqualTo("Sam");
post.Owner.Id.IsEqualTo(99);
var docs = connection.Query<Document, Customer, Document>(
"select Id, SomeText, CustomerId as [Id], CustomerName as [Name] from Documents",
(doc, cust) => { doc.Customer = cust; return doc; }).ToList();

Categories