I am using ef-core 2.1, I have the following simplified entities where one Account maps to zero or more Attribute objects:
public class Account
{
public int Id { get; set; }
public int LongId { get; set; }
public List<Attribute> Attributes { get; set; }
}
public class Attribute
{
public int Id { get; set; }
public string Name { get; set; }
public string Value { get; set; }
public Account Account { get; set; }
}
I have an initial collection of strings that resemble an Attribute.Value for a given fixed Attribute.Name, I want to find a second associated Attribute object from the same parent Account and obtain its Attribute.Value.
I want to left join the ef entities against the initial string collection so I can easily infer:
Whether no corresponding Account exists or an Account exists without the related Attribute objects (both equate to the same use case).
If an Account exists and it contains all the required Attribute objects, I want to obtain the value of the secondary Attribute.
Without LINQ and ef, I run the following SQL query which ignores the parent Account and produces the result set I want:
CREATE TABLE #Temp
(
id nvarchar(20) not null
);
INSERT INTO #Temp (id) VALUES ('cejawq'), ('issokq'), ('cqlpjq'), ('mbgzvi'), ('wqwlff'), ('iedifh');
SELECT t.[Id], attr2.[Value]
FROM #Temp t
LEFT OUTER JOIN [dbo].[Attributes] attr1
ON t.[Id]=attr1.[Value]
AND attr1.[Name]='uid'
LEFT OUTER JOIN [dbo].[Attributes] attr2
ON attr1.[AccountId]=attr2.[AccountId]
AND attr2.[Name]='objType';
I get the following result set:
id|objType
-----------
cejawq|ext
issokq|ext
cqlpjq|int
mbgzvi|int
wqwlff|ext
iedifh|null
I am struggling with mapping this to efficient LINQ such that the SQL generated produces the result set remotely and ships back data that I can project to an equivalent anonymous type. Do I need to care about the parent objects in the LINQ case? I don't have an index on the Attribute.Value column.
The Attributes table contains the following data:
Id|Name |Value |AccountId
1 |uid |cejawq|1
2 |objType|ext |1
3 |uid |issokq|2
4 |objType|ext |2
5 |uid |cqlpjq|3
6 |objType|int |3
7 |uid |mbgzvi|4
8 |objType|int |4
9 |uid |wqwlff|5
10|objType|ext |5
Since the EF Core does not support joins with in memory sequences (yet), you can split the query in two parts - one which takes the data server side ([Attributes to [Attributes join) using in memory collection as filter (SQL IN through LINQ Contains method), and second which performs left join in memory with the result of the db query:
DbContext db = ...;
var uids = new [] { "cejawq", "issokq", "cqlpjq", "mbgzvi", "wqwlff", "iedifh" };
var dbQuery =
from attr1 in db.Set<Attribute>()
where attr1.Name == "uid" && uids.Contains(attr1.Value)
join attr2 in db.Set<Attribute>()
on new { AccountId = attr1.Account.Id, Name = "objType" }
equals new { AccountId = attr2.Account.Id, attr2.Name }
into attr2Group from attr2 in attr2Group.DefaultIfEmpty() // left outer join
select new { uid = attr1.Value, objType = attr2.Value };
var query =
from uid in uids
join dbResult in dbQuery on uid equals dbResult.uid
into dbResultGroup from dbResult in dbResultGroup.DefaultIfEmpty() // left outer join
select new { uid, dbResult?.objType };
var result = query.ToList();
It translates to a single db query like this:
SELECT [attr1].[Value] AS [uid], [attr2].[Value] AS [objType]
FROM [Attributes] AS [attr1]
LEFT JOIN [Attributes] AS [attr2] ON ([attr1].[AccountId] = [attr2].[AccountId]) AND (N'objType' = [attr2].[Name])
WHERE ([attr1].[Name] = N'uid') AND [attr1].[Value] IN (N'cejawq', N'issokq', N'cqlpjq', N'mbgzvi', N'wqwlff', N'iedifh')
I've a Many to Many relation between a student (etudiant) and a course (cours) table which is called studentCours in SQL but not use as a model in Entity Framework (i've taken the habit of it now, even if i think its weird).
I need to show the result of the student (which is on another table etuResult) but the student may not have any result (nullable int) yet as the year is still in progress !
I'm able to get the value of studentCours and show them and also of studentResult, but i'm not able to show the concatenation of both (and leave the result empty or with other value than a number between 1 and 20).
On the following picture you can see :
On the left : the student with the id 3 has 2 cours with the id 1 and 10 (i dont know why the course_id 1 appears twice...) => These are the course he's following.
On the right : The student with the id 3 has one result for the course_id 10 and has the result 10/20
Here are my two queries to get the data :
var etuCours = from c in bdd.cours
where c.etudiant.Any(e => e.etudiant_id == id)
select new resultCours { cours_id = c.cours_id, libelle = c.libelle, code = c.code, annee = c.annee };
IEnumerable<resultCours> etuResult = from er in bdd.etuResult
where er.etudiant_id == id
select new resultCours { cours_id = er.cours_id, cote = er.cote };
I'm trying to get a list of servers thay may or may not belong to 1 or more groups to display in a grid.
Example
ServerID IP GroupID
1 192.168.1.44 1
1 192.168.1.44 10
2 192.168.1.45 1
3 192.168.1.46 2
4 192.168.1.47 null
5 192.168.1.48 null
If I have no records In the GroupServer Table. (Since there is no groups or groups exist but they are not assigned) I expect to get something like this:
ServerID IP GroupID
1 192.168.1.44 null
2 192.168.1.45 null
3 192.168.1.46 null
4 192.168.1.47 null
5 192.168.1.48 null
Since is a Many-to-Many relationship. I have
Group Table
Server Table
GroupServer Table
I could not find a LINQ Pivot Table example.
So I tried to buid my own.
var query = (from sg in context.ServerGroups
join servers in context.Servers on sg.ServerID equals servers.ID
join groups in context.Groups on sg.GroupID equals groups.ID
into serverxgroup
from gAddrBilling in serverxgroup.DefaultIfEmpty()
select new
{
ServerID = sg.ServerID,
ServerIP = server.IP,
GroupID = sg.GroupID
});
The Query above does not retrieve anything
And I quiet dont understand what the "from gAddrBilling" is for. Since I modify a snippet I was trying to make work. So I wonder if someone has already faced a problem like this and give me some hint, snippet or advice about what is what I'm missing.
Thank you.
First, this is not a pivot query, but a regular query on many-to-may relationship via explicit junction table.
Second, looks like you are using Entity Framework, in which case you'd better define and use navigation properties rather than manual joins.
Third, and the most important, the structure of the query is wrong. If you want to get a list of servers that may or may not belong to 1 or more groups, then you should start your query from Servers (the table which records you want to be always included, not from link table where some ServerID are missing) and then use left outer joins to the other tables like this:
var query =
from s in servers in context.Servers
join sg in context.ServerGroups on s.ID equals sg.ServerID
into s_sg from sg in s_sg.DefaultIfEmpty() // make the above LEFT OUTER JOIN
// You can remove the next two lines if all you need is the GroupId
// and keep them if you need some other Group field in the select
join g in context.Groups on sg.GroupID equals g.ID
into sg_g from g in sg_g.DefaultIfEmpty() // make the above LEFT OUTER JOIN
select new
{
ServerID = s.ID,
ServerIP = s.IP, // or sg.IP?
GroupID = (int?)sg.GroupID
};
I'm trying to use the multimapping feature of Dapper to return a list of ProductItems and associated Customers.
[Table("Product")]
public class ProductItem
{
public decimal ProductID { get; set; }
public string ProductName { get; set; }
public string AccountOpened { get; set; }
public Customer Customer { get; set; }
}
public class Customer
{
public decimal CustomerId { get; set; }
public string CustomerName { get; set; }
}
My Dapper code:
var sql = #"select * from Product p
inner join Customer c on p.CustomerId = c.CustomerId
order by p.ProductName";
var data = con.Query<ProductItem, Customer, ProductItem>(
sql,
(productItem, customer) => {
productItem.Customer = customer;
return productItem;
},
splitOn: "CustomerId,CustomerName"
);
This works fine, but I seem to have to add the complete column list to the "splitOn" parameter to return all the customers' properties. If I don't add "CustomerName", it returns null. Am I misunderstanding the core functionality of the multimapping feature? I don't want to have to add a complete list of column names each time.
I just ran a test that works fine:
var sql = "select cast(1 as decimal) ProductId, 'a' ProductName, 'x' AccountOpened, cast(1 as decimal) CustomerId, 'name' CustomerName";
var item = connection.Query<ProductItem, Customer, ProductItem>(sql,
(p, c) => { p.Customer = c; return p; }, splitOn: "CustomerId").First();
item.Customer.CustomerId.IsEqualTo(1);
The splitOn param needs to be specified as the split point, it defaults to Id. If there are multiple split points, you will need to add them in a comma delimited list.
Say your recordset looks like this:
ProductID | ProductName | AccountOpened | CustomerId | CustomerName
--------------------------------------- -------------------------
Dapper needs to know how to split the columns in this order into 2 objects. A cursory look shows that the Customer starts at the column CustomerId, hence splitOn: CustomerId.
There is a big caveat here, if the column ordering in the underlying table is flipped for some reason:
ProductID | ProductName | AccountOpened | CustomerName | CustomerId
--------------------------------------- -------------------------
splitOn: CustomerId will result in a null customer name.
If you specify CustomerId,CustomerName as split points, dapper assumes you are trying to split up the result set into 3 objects. First starts at the beginning, second starts at CustomerId, third at CustomerName.
Our tables are named similarly to yours, where something like "CustomerID" might be returned twice using a 'select *' operation. Therefore, Dapper is doing its job but just splitting too early (possibly), because the columns would be:
(select * might return):
ProductID,
ProductName,
CustomerID, --first CustomerID
AccountOpened,
CustomerID, --second CustomerID,
CustomerName.
This makes the splitOn: parameter not so useful, especially when you're not sure what order the columns are returned in. Of course you could manually specify columns... but it's 2017 and we just rarely do that anymore for basic object gets.
What we do, and it's worked great for thousands of queries for many many years, is simply use an alias for Id, and never specify splitOn (using Dapper's default 'Id').
select
p.*,
c.CustomerID AS Id,
c.*
...voila! Dapper will only split on Id by default, and that Id occurs before all the Customer columns. Of course it will add an extra column to your return resultset, but that is extremely minimal overhead for the added utility of knowing exactly which columns belong to what object. And you can easily expand this. Need address and country information?
select
p.*,
c.CustomerID AS Id,
c.*,
address.AddressID AS Id,
address.*,
country.CountryID AS Id,
country.*
Best of all, you're clearly showing in a minimal amount of SQL which columns are associated with which object. Dapper does the rest.
Assuming the following structure where '|' is the point of splitting and Ts are the entities to which the mapping should be applied.
TFirst TSecond TThird TFourth
------------------+-------------+-------------------+------------
col_1 col_2 col_3 | col_n col_m | col_A col_B col_C | col_9 col_8
------------------+-------------+-------------------+------------
Following is the Dapper query that you will have to write.
Query<TFirst, TSecond, TThird, TFourth, TResut> (
sql : query,
map: Func<TFirst, TSecond, TThird, TFourth, TResut> func,
parma: optional,
splitOn: "col_3, col_n, col_A, col_9")
So we want for TFirst to map to col_1 col_2 col_3, for TSecond to col_n col_m ...
The splitOn expression translates to:
Start mapping of all columns into TFirst till you find a column named or aliased as 'col_3', and also include 'col_3' into the mapping result.
Then start mapping into TSecond all columns starting from 'col_n' and continue mapping till new separator is found, which in this case is 'col_A', and mark the start of TThird mapping and so on.
The columns of the SQL query and the props of the mapping object are in a 1:1 relation (meaning that they should be named the same). If the column names resulting from the SQL query are different, you can alias them using the 'AS [Some_Alias_Name]' expression.
If you need to map a large entity write each field must be a hard task.
I tried #BlackjacketMack answer, but one of my tables has an Id Column other ones not (I know it's a DB design problem, but ...) then this insert an extra split on dapper, that's why
select
p.*,
c.CustomerID AS Id,
c.*,
address.AddressID AS Id,
address.*,
country.CountryID AS Id,
country.*
Doesn't work for me. Then I ended with a little change to this, just insert an split point with a name that doesn't match with any field on tables, In may case changed as Id by as _SplitPoint_, the final sql script looks like this:
select
p.*,
c.CustomerID AS _SplitPoint_,
c.*,
address.AddressID AS _SplitPoint_,
address.*,
country.CountryID AS _SplitPoint_,
country.*
Then in dapper add just one splitOn as this
cmd =
"SELECT Materials.*, " +
" Product.ItemtId as _SplitPoint_," +
" Product.*, " +
" MeasureUnit.IntIdUM as _SplitPoint_, " +
" MeasureUnit.* " +
"FROM Materials INNER JOIN " +
" Product ON Materials.ItemtId = Product.ItemtId INNER JOIN " +
" MeasureUnit ON Materials.IntIdUM = MeasureUnit.IntIdUM " +
List < Materials> fTecnica3 = (await dpCx.QueryAsync<Materials>(
cmd,
new[] { typeof(Materials), typeof(Product), typeof(MeasureUnit) },
(objects) =>
{
Materials mat = (Materials)objects[0];
mat.Product = (Product)objects[1];
mat.MeasureUnit = (MeasureUnit)objects[2];
return mat;
},
splitOn: "_SplitPoint_"
)).ToList();
There is one more caveat. If CustomerId field is null (typically in queries with left join) Dapper creates ProductItem with Customer = null. In the example above:
var sql = "select cast(1 as decimal) ProductId, 'a' ProductName, 'x' AccountOpened, cast(null as decimal) CustomerId, 'n' CustomerName";
var item = connection.Query<ProductItem, Customer, ProductItem>(sql, (p, c) => { p.Customer = c; return p; }, splitOn: "CustomerId").First();
Debug.Assert(item.Customer == null);
And even one more caveat/trap. If you don't map the field specified in splitOn and that field contains null Dapper creates and fills the related object (Customer in this case). To demonstrate use this class with previous sql:
public class Customer
{
//public decimal CustomerId { get; set; }
public string CustomerName { get; set; }
}
...
Debug.Assert(item.Customer != null);
Debug.Assert(item.Customer.CustomerName == "n");
I do this generically in my repo, works good for my use case. I thought I'd share. Maybe someone will extend this further.
Some drawbacks are:
This assumes your foreign key properties are the name of your child object + "Id", e.g. UnitId.
I have it only mapping 1 child object to the parent.
The code:
public IEnumerable<TParent> GetParentChild<TParent, TChild>()
{
var sql = string.Format(#"select * from {0} p
inner join {1} c on p.{1}Id = c.Id",
typeof(TParent).Name, typeof(TChild).Name);
Debug.WriteLine(sql);
var data = _con.Query<TParent, TChild, TParent>(
sql,
(p, c) =>
{
p.GetType().GetProperty(typeof (TChild).Name).SetValue(p, c);
return p;
},
splitOn: typeof(TChild).Name + "Id");
return data;
}
I would like to note a very important aspect: the property name within the Entity must match the select statement. Another aspect of splitOn is how it looks for Id by default, so you don't have to specify it unless your naming is something like CustomerId, instead of Id. Let's look at these 2 approaches:
Approach 1
Entity Customer : Id Name
Your query should be something like:
SELECT c.Id as nameof{Customer.Id}, c.Foo As nameof{Customer.Name}.
Then your mapping understands the relationship between the Entity and the table.
Approach 2
Entity Customer: CustomerId, FancyName
Select c.Id as nameof{Customer.CustomerId}, c.WeirdAssName As nameof{Customer.FancyName}
and at the end of the mapping, you have to specify that the Id is the CustomerId by using the SplitOn.
I had an issue where I was not getting my values even though the mapping was correct technically because of a mismatch with the SQL statement.
I have the SQL query
with c as (
select categoryId,parentId, name,0 as [level]
from task_Category b
where b.parentId is null
union all
select b.categoryId,b.parentId,b.name,[level] + 1
from task_Category b join c on b.parentId =
c.categoryId)
select name,[level],categoryId,parentId
as item
from c
and I want to convert it to LINQ to SQL, yet my LINQ skills are not there yet. Could someone please help me convert this. It's the with and union statements that are making this a bit more complex for me.
Any help appreciated.
That is a recursive CTE. LINQ to SQL does not support recursive CTEs yet (and probably never will). Sorry!
As a workaround you can leave the query as it is in SQL and put it in a stored procedure. You can then call that stored procedure from LINQ to SQL.
LINQ-to-SQL only supports basic queries; CTE and recursion are not included in this. Therefore, here's my high-tech translation to LINQ-to-SQL:
var data = ctx.ExecuteQuery<MyStub>(#"
with c as (
select categoryId,parentId, name,0 as [level]
from task_Category b
where b.parentId is null
union all
select b.categoryId,b.parentId,b.name,[level] + 1
from task_Category b join c on b.parentId =
c.categoryId)
select name,[level],categoryId,parentId
as item
from c").ToList();
with
class MyStub {
public string name {get;set;}
public int level {get;set;}
public int categoryId {get;set;}
public int parentId {get;set;}
}