"MultiLevels" Mapping Dapper using SplitOn - c#

Most of examples/question just introduce solutions to map "only one" level of the query using split on like this:
var sql = "SELECT P.Id, P.FirstName, P.LastName, " +
"A.Id AS AddressId, A.StreetNumber, A.StreetName, A.City, A.State " +
"FROM People P INNER JOIN Addresses A ON A.AddressId = P.AddressId; ";
db.Query<Person, Address, Person>( sql, (person, address) => {
person.Address = address;
return person; }, splitOn: "AddressId" ).ToList();
I have a query like this one (just an example):
Select * from Country C
inner join State S
on C.CountryId = S.CountryId
inner join City Ct
on S.StateId = Ct.StateId
How could I map it using dapper to my Model/Class?

There is no out of box solution for your needs in Dapper or its extensions. Dapper maps every single row in result set separately. So you need some extra mapping in order to do something like what you want. You can do it manually after getting results of Query with multiple splitOn. Or use some mapping tool. Please, consider this question with various answers. Adapted to your case the solution(with Slapper.Automapper mapping) would be:
[Test]
public async Task MultipleSplitOn()
{
// Arrange
using (var conn =new SqlConnection("Data Source=YourDb"))
{
await conn.OpenAsync();
var sql = #"SELECT TOP 10 c.[Id] as CountryId
,c.[Name]
,s.[Id] as States_StateId
,s.[Name] as States_Name
,ct.[Id] as States_Cities_CityId
,ct.[Name] as States_Cities_Name
FROM Country c
JOIN State s ON s.[CountryId] = c.[Id]
JOIN City ct ON ct.[StateId] = s.[Id] ";
// Act
dynamic result = await conn.QueryAsync<dynamic>(sql);
Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(Country), new [] { "CountryId" });
Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(State), new [] { "StateId" });
Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(City), new [] { "CityId" });
var countries = (Slapper.AutoMapper.MapDynamic<Country>(result) as IEnumerable<Country>).ToList();
//Assert
Assert.IsNotEmpty(countries);
foreach (var country in countries)
{
Assert.IsNotEmpty(country.States);
foreach (var state in country.States)
{
Assert.IsNotEmpty(state.Cities);
}
}
}
}
public class Country
{
public int CountryId { get; set; }
public string Name { get; set; }
public List<State> States { get; set; }
}
public class State
{
public int StateId { get; set; }
public string Name { get; set; }
public List<City> Cities { get; set; }
}
public class City
{
public int CityId { get; set; }
public string Name { get; set; }
}

Related

C# How do you query an object with list properties by joining two tables

public class CategoryDomainModel
{
public string? _id { get; set; }
public string? RestaurantID { get; set; }
public string? CategoryName { get; set; }
public Enums.Status Status { get; set; }
}
public class MenuItemDomainModel
{
public string? _id { get; set; }
public string? CategoryID { get; set; }
public string? ImageLink { get; set; }
public string? ItemName { get; set; }
public string? ItemDescription { get; set; }
}
Imagine you have these two tables in mongodb and a category has many menus.
When you want to join the two tables and get all categories + menus by Restaurant ID with a result like this
public class CategoryAndMenusDomainModel
{
public string? _id { get; set; }
public string? RestaurantID { get; set; }
public string? CategoryName { get; set; }
public Enums.Status Status { get; set; }
public List<MenuItemDomainModel>? Menus { get; set; }
}
How do you go about it?
Ive tried:
var categoryCollection = database.GetCollection<CategoryDomainModel>("Categories");
var menuCollection = database.GetCollection<MenuItemDomainModel>("Menus");
var categoriesAndMenus = (from b in categoryCollection.AsQueryable()
join c in menuCollection.AsQueryable()
on b._id equals c.CategoryID
where b.RestaurantID == restautantID
select new CategoryAndMenusDomainModel
{
_id = b._id,
CategoryName = b.CategoryName,
RestaurantID = b.RestaurantID,
Menus = new List<MenuItemDomainModel>
{
new MenuItemDomainModel
{
_id = c._id,
ItemName = c.ItemName,
ItemDescription = c.ItemDescription
}
}
}).ToList();
But its throwing an exception:
"$project or $group does not support new List`1()
Consider this workaround:
var categoriesAndMenus = (from b in categoryCollection.AsQueryable()
join c in menuCollection
on b._id equals c.CategoryID
where b.RestaurantID == restautantID
select new
{
_id = b._id,
CategoryName = b.CategoryName,
RestaurantID = b.RestaurantID,
C_id = c._id,
C_CategoryName = c.ItemName,
C_ItemDescription = c.ItemDescription
}
)
.ToList()
// below step is client side tranformation, but all required data is already received from the server, so it's just minor projection
.Select(i => new CategoryAndMenusDomainModel
{
_id = i._id,
CategoryName = i.CategoryName,
RestaurantID = i.RestaurantID,
Menus = new List<MenuItemDomainModel>
{
new MenuItemDomainModel
{
_id = i._id,
ItemName = i.CategoryName,
ItemDescription = i.C_ItemDescription
}
}
});
The generated MQL query for this case will be:
{
"aggregate": "Categories",
"pipeline": [
{
"$lookup":
{
"from": "Menus",
"localField": "_id",
"foreignField": "CategoryID",
"as": "c"
}
},
{ "$unwind": "$c" },
{ "$match": { "RestaurantID": "1" } },
{
"$project":
{
"_id": "$_id",
"CategoryName": "$CategoryName",
"RestaurantID": "$RestaurantID",
"C_id": "$c._id",
"C_CategoryName": "$c.ItemName",
"C_ItemDescription": "$c.ItemDescription"
}
}
]
}
I'm 99% confident that client side projection can be moved to the server side, for example via a raw MQL query with $map, see MongoDB project into an array, but it will require more investigation
Linq translates the C# code into SQL. SQL doesn't know about C# lists, so it doesn't know how to format that. You need to do this in two steps: 1) get the data from the database in flat rows, then 2) in C#, put in into the structure you want.
Here I've used the standard Northwind database, but you should be able to adapt it to your data.
var rows = (from c in Categories
join p in Products on c.CategoryID equals p.CategoryID
//where c.CategoryID == 1
select new
{
_id = c.CategoryID,
CategoryName = c.CategoryName,
RestaurantID = c.CategoryID,
menu_id = p.ProductID,
ItemName = p.ProductName,
ItemDescription = p.ProductName
}).ToList();
var categoriesAndMenus =
(from r in rows
group r by r._id into grp
let cat = grp.First()
select new
{
_id = cat._id,
cat.CategoryName,
cat.RestaurantID,
Menu = grp.Select(g =>new
{
_id = g.menu_id,
g.ItemName,
g.ItemDescription
} )
}).ToList();

Dapper query object with a child of IEnumerable

I am trying query all countries and in each country object it would fill up the provinces.
I have the following Classes
public class Country
{
public int Countryid { get; set; }
public string CountryName { get; set; }
public IEnumerable<Province> Provinces { get; set; }
}
public class Province
{
public int ProvinceId { get; set; }
public string ProvinceName { get; set; }
}
public IEnumerable<Country> GetCountries()
{
var query = #"
SELECT [Country].[CountryId], [Country].[Name] as CountryName, [Province].[ProvinceId], [Province].[Name] as ProvinceName
FROM [Province]
RIGHT OUTER JOIN [Country] ON [Province].[CountryId] = [Country].[CountryId]
WHERE [Country].[CountryId] > 0";
return _connection.Query<Country, Province, Country>(query, (country, province) =>
{
country.Provinces = country.Provinces.Concat(new List<Province> { province });
return country;
}, null);
}
The error that I get is the following:
System.ArgumentException: 'When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id
Parameter name: splitOn'
I have been following this example:
https://gist.github.com/Lobstrosity/1133111
From my perspective, I don't see what I did much different besides mine being an outer join which I think should not matter, the result format is about the same.
Why do I need the split on and can this work without it?
IIRC, I did something like this.
var query = #"
SELECT [Country].[CountryId], [Country].[Name] as CountryName, [Province].[ProvinceId], [Province].[Name] as ProvinceName
FROM [Province]
RIGHT OUTER JOIN [Country] ON [Province].[CountryId] = [Country].[CountryId]
WHERE [Country].[CountryId] > 0";
List<Country> countries = new List<Country>();
_connection.Query<Country, Province, Country>(query, (country, province) =>
{
Country lastCountry = countries.FirstOrDefault(d => d.CountryId == country.Id);
if(lastCountry == null)
{
countries.Add(country);
lastCountry = country;
}
lastCountry.Provinces = lastCountry.Provinces.Concat(new List<Province> { province });
return lastCountry;
}, null);
return countries;
I typed this out in LinqPad, so you will need to debug and check it is correct, been a long time since I used Dapper in anger

Querying into a complex object with Dapper

I have a Customer class with the following properties:
public int Id { get; set; }
public string Name { get; set; }
public int AddressId { get; set; }
public Address Address { get; set; }
My goal is to write a Dapper query that will use an Inner Join to populate the entire Address property within each Customer that is returned.
Here is what I have and it is working but I am wondering if this is the cleanest/simplest way to do it:
StringBuilder sql = new StringBuilder();
using (var conn = GetOpenConnection())
{
sql.AppendLine("SELECT c.Id, c.Name, c.AddressId, a.Address1, a.Address2, a.City, a.State, a.ZipCode ");
sql.AppendLine("FROM Customer c ");
sql.AppendLine("INNER JOIN Address a ON c.AddressId = a.Id ");
return conn.Query<Customer, Address, Customer>(
sql.ToString(),
(customer, address) => {
customer.Address= address;
return userRole;
},
splitOn: "AddressId"
).ToList();
}
I have some concern about adding another property such as:
public Contact Contact { get; set; }
I am not sure how I would switch the syntax above to populate both Address and Contact.
I have coded using Dapper version 1.40 and I have written queries like the way below, I haven't got any issues to populate mote more than one object, but I have faced a limit of 8 different classes those I can map in a query.
public class Customer {
public int Id { get; set; }
public string Name { get; set; }
public int AddressId { get; set; }
public int ContactId { get; set; }
public Address Address { get; set; }
public Contact Contact { get; set; }
}
public class Address {
public int Id { get; set; }
public string Address1 {get;set;}
public string Address2 {get;set;}
public string City {get;set;}
public string State {get;set;}
public int ZipCode {get;set;}
public IEnumerable<Customer> Customer {get;set;}
}
public class Contact {
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<Customer> Customer {get;set;}
}
using (var conn = GetOpenConnection())
{
var query = _contextDapper
.Query<Customer, Address, Contact, Customer>($#"
SELECT c.Id, c.Name,
c.AddressId, a.Id, a.Address1, a.Address2, a.City, a.State, a.ZipCode,
c.ContactId, ct.Id, ct.Name
FROM Customer c
INNER JOIN Address a ON a.Id = c.AddressId
INNER JOIN Contact ct ON ct.Id = c.ContactId",
(c, a, ct) =>
{
c.LogType = a;
c.Contact = ct;
return c;
}, splitOn: "AddressId, ContactId")
.AsQueryable();
return query.ToList();
}
Take a look in my example with a big query, note that Each Query line It's a different object.
public List<Appointment> GetList(int id)
{
List<Appointment> ret;
using (var db = new SqlConnection(connstring))
{
const string sql = #"SELECT AP.[Id], AP.Diagnostics, AP.Sintomns, AP.Prescription, AP.DoctorReport, AP.AddressId,
AD.Id, AD.Street, AD.City, AD.State, AD.Country, AD.ZIP, Ad.Complement,
D.Id, D.Bio, d.CRMNumber, D.CRMNumber, D.CRMState,
P.Id,
S.Id, S.Name,
MR.Id, MR.Alergies, MR.BloodType, MR.DtRegister, Mr.HealthyProblems, MR.HealthyProblems, MR.Height, MR.MedicalInsuranceNumber, MR.MedicalInsuranceUserName, MR.Medications, MR.Weight,
MI.Id, MI.Name
from Appointment AP
inner join [Address] AD on AD.Id = AP.AddressId
inner join Doctor D on D.Id = AP.DoctorId
inner join Patient P on P.Id = AP.PatientId
left join Speciality S on S.Id = D.IDEspeciality
left join MedicalRecord MR on MR.Id = P.MedicalRecordId
left join MedicalInsurance MI on MI.Id = MR.MedicalInsuranceId
where AP.Id = #Id
order by AP.Id desc";
ret = db.Query<Appointment, Address, Doctor, Patient, Speciality, MedicalRecord, MedicalInsurance, Appointment>(sql,
(appointment, address, doctor, patient, speciality, medicalrecord, medicalinsurance) =>
{
appointment.Address = address;
appointment.Doctor = doctor;
appointment.Patient = patient;
appointment.Doctor.Speciality = speciality;
appointment.Patient.MedicalRecord = medicalrecord;
appointment.Patient.MedicalRecord.MedicalInsurance = medicalinsurance;
return appointment;
}, new { Id = id }, splitOn: "Id, Id, Id, Id, Id, Id").ToList();
}
return ret;
}

Best way to use LINQ to Entities with a Collection Property

Let's say I have the following model:
As you can see each city has one or more train stations.
And I have the following type:
public class CityDTO
{
public string CityName { get; set; }
public string StateName { get; set; }
public List<string> Trainstations { get; set; }
}
Now the cool thing about LINQ is that I can query the Entity Framework and return a new collection of my type:
public List<CityDTO> GetCities()
{
using (var db = new CityDataContext())
{
var cities = db.Cities;
var trainstations = db.TrainStations;
var query =
(from city in cities
select new CityDTO
{
CityName = city.Name,
StateName = city.State
}).ToList();
return query;
}
}
The question is: can I also return a collection of train station names in the same LINQ query to add to my CityDTO class?
public List<CityDTO> GetCities()
{
using (var db = new CityDataContext())
{
var cities = db.Cities;
var trainstations = db.TrainStations;
var query =
(from city in cities
join trainstation in trainstations
on city.CityId
equals trainstation.CityId into orderGroup
select new CityDTO
{
CityName = city.Name,
StateName = city.State
//assign List<string> of trainstation names to CityDTO.Trainstations
}).ToList();
return query;
}
}
The other problem with the Join above is that it will return the same city name multiple times (for each of its train stations), and I only want one instance of CityDTO per city.
Is this best done with two LINQ statements? One to get a new list of cities, and the second to get the list of train stations for each?
#haim770, your answer helped me on the right track. Just as a matter of interest, I had to change two more things.
When I just add the Select projector, I get the following run time error
So the Select expects the projected type to be IEnumerable, so I could have done this and all would be cool:
public class CityDTO
{
public string CityName { get; set; }
public string StateName { get; set; }
public IEnumerable<string> Trainstations { get; set; } //changed from List<> to IEnumerable<>
}
Now this works fine:
public List<CityDTO> GetCities()
{
using (var db = new CitysDataEntities())
{
var cities = db.Cities;
var query =
(from city in cities
select new CityDTO
{
CityName = city.Name,
Trainstations = city.TrainStations.Select(ts => ts.Name) //now this works
}).ToList();
return query;
}
}
However, after some further reading, I decided to use IQueryable which also has a Select method.
The reason for this is that IQueryable.Select is under System.Linq, while IEnumerable.Select is in the System.Collections Namespace. This is important as IQueryable.Select is optimized to execute with all filters on the server and return only the result to the client, while IEnumerable.Select loads the objects to the client first before filtering.
IEnumerable Vs IQueryable
So the result is this
public class CityDTO
{
public string CityName { get; set; }
public string StateName { get; set; }
public IQueryable<string> Trainstations { get; set; } // changed to IQueryable
}
and
public List<CityDTO> GetCities()
{
using (var db = new CitysDataEntities())
{
var cities = db.Cities;
var query =
(from city in cities
select new CityDTO
{
CityName = city.Name,
Trainstations = city.TrainStations.Select(ts => ts.Name).AsQueryable() // return AsQueryable
}).ToList();
return query;
}
}
Now when I add filters they will be applied server-side.
MSDN - Queryable.Select
MSDN - Enumerable.Select
The Join operation here is implicit because there is a navigation-property from City to TrainStations so EF will be able to automatically retrieve the stations for you :
(from city in cities
select new CityDTO
{
CityName = city.Name,
StateName = city.State,
Trainstations = city.TrainStations.Select(ts => ts.Name).ToList()
})
The above is using Select() to project the list of train stations into a list of string based on the Name property.
See MSDN
Use AutoMapper to project collection property to IList.
Queryable.ProjectTo<UserListDto>().ToArray();
Entity
public class User
{
public string Name { get; set; }
public virtual ICollection<UserRole> Roles { get; set; }
}
Dto
[AutoMapFrom(typeof(User))]
public class UserListDto
{
public string Name { get; set; }
public IList<UserRoleOutput> Roles { get; set; }
[AutoMapFrom(typeof(UserRole))]
public class UserRoleOutput
{
public int RoleId { get; set; }
}
}

Get Linq Subquery into a User Defined Type

I have a linq query, which is further having a subquery, I want to store the result of that query into a user defined type, my query is
var val = (from emp in Employees
join dept in Departments
on emp.EmployeeID equals dept.EmployeeID
select new Hello
{
EmployeeID = emp.EmployeeID
Spaces = (from order in Orders
join space in SpaceTypes
on order.OrderSpaceTypeID equals space.OrderSpaceTypeID
where order.EmployeeID == emp.EmployeeID group new { order, space } by new { order.OrderSpaceTypeID, space.SpaceTypeCode } into g
select new
{
ID = g.Key.SpaceTypeID,
Code = g.Key.SpaceTypeCode,
Count = g.Count()
})
}).ToList();
Definition for my Hello class is
public class Hello
{
public IEnumerable<World> Spaces { get; set; }
public int PassengerTripID { get; set; }
}
Definition for my World class is
public class World
{
public int ID { get; set; }
public string Code { get; set; }
public int Count { get; set; }
}
You are creating anonymous object but you need to specify type name World
select new World
{
ID = g.Key.SpaceTypeID,
Code = g.Key.SpaceTypeCode,
Count = g.Count()
})

Categories