I have 3 tables linked by Foreign Keys: ChangeSet, ObjectChanges, PropertyChanges. These tables have 1-To-Many Relationships with each other. I need to join and project and flatten the results into an anonymous type.
We use Entity Framework at the data layer and I essentially need to make the following Query with linq.
select c.Id as ChangeSetId,
c.Timestamp,
c.Author_Id,
u.Name as [User],
o.id as ObjectChangeId,
o.TypeName,
o.ObjectReference as EntityId,
o.DisplayName,
p.Id as PropertyChangeId,
p.PropertyName,
p.ChangeType,
p.OriginalValue,
p.Value
from ChangeSets c
inner join ObjectChanges o
on c.Id = o.ChangeSetId
left join PropertyChanges p
on p.ObjectChangeId = o.Id
inner join Users u
on u.Id = c.Author_Id
order by c.id desc
The Method in question however looks like this:
GetAllWhereExpression(Expression<Func<ChangeSet, bool>> expression)
The expression in this case is likely to be a Where o.EntityId = [Some Value] and c.TimeStamp > X and < Y.
I got very close I felt in linq with the following but couldn't figure out how to inject the expression: (The .GetRepository().Entities is basically DbSet)
var foo = from c in _uow.GetRepository<ChangeSet>().Entities
join o in _uow.GetRepository<ObjectChange>().Entities on c.Id equals o.ChangeSetId
join p in _uow.GetRepository<PropertyChange>().Entities on o.Id equals p.ObjectChangeId
where expression // This Line Not Valid
select new
{
ChangeSetId = c.Id,
Timestamp = c.Timestamp,
User = c.User.DisplayName,
EntityType = o.TypeName,
EntityValue = o.DisplayName,
Property = p.PropertyName,
OldValue = p.OriginalValue,
NewValue = p.Value
};
I'd prefer to use Lambda syntax but I can't figure out how to construct it. I know I need SelectMany to project and flatten the results but I can't figure out how to use them within the anonymous type for the subcollections:
var queryable = _uow.GetRepository<ChangeSet>().Entities // This is basically the DbSet<ChangeSet>()
.Where(expression)
.SelectMany(c => new
{
ChangeSetId = c.Id,
Timestamp = c.Timestamp,
User = c.User.DisplayName,
EntityType = c.ObjectChanges.SelectMany(o => o.TypeName), //Doesn't work, turns string into char array
//PropertyName = c. this would be a 1 to many on the entity
}
)
How do I craft the linq to produce basically the same results as the sql query?
Here is how it may look like in method syntax.
_uow.GetRepository<ChangeSet>().Entities
.Where(expression)
.Join(_uow.GetRepository<ObjectChanges>().Entities, cs => cs.Id, oc => oc.ChangeSetId,
(cs, oc) => new { cs, oc })
.Join(_uow.GetRepository<PropertyChanges>().Entities, outer => outer.oc.Id, pc => pc.ObjectChangeId,
(outer, pc) => new { cs = outer.cs, oc = outer.cs, pc })
.Join(_uow.GetRepository<User>().Entities, outer => outer.cs.Author_Id, u => u.Id,
(outer, u) => new {
ChangeSetId = outer.cs.Id,
Timestamp = outer.cs.Timestamp,
User = u.DisplayName,
EntityType = outer.oc.TypeName,
EntityValue = outer.oc.DisplayName,
Property = outer.pc.PropertyName,
OldValue = outer.pc.OriginalValue,
NewValue = outer.pc.Value
})
Please follow the given demonstration, Four entities joined by LINQ extension methods.
N.B: .Join use for Inner Join
var foo=_uow.GetRepository<ChangeSet>().Entities
.Join(_uow.GetRepository<ObjectChange>().Entities,
c=>c.Id,
o=>o.ChangeSetId,
(c,o)=>new{ChangeSet=c,ObjectChange=o})
.Join(_uow.GetRepository<PropertyChange>().Entities,
o=>o.ObjectChange.Id,
p=>p.ObjectChangeId,
(o,p)=>new{o.ChangeSet,o.ObjectChange,PropertyChange=p})
.Join(_uow.GetRepository<Users>().Entities,
c=>c.ChangeSet.Author_Id,
u=>u.Id,
(c,u)=>new{c.ChangeSet,c.ObjectChange,c.PropertyChange,User=u})
.Select(x=>new
{
ChangeSetId=x.ChangeSet.Id,
x.ChangeSet.Timestamp,
x.ChangeSet.Author_Id,
User=x.User.Name,
ObjectChangeId=x.ObjectChange.id,
x.ObjectChange.TypeName,
EntityId=x.ObjectChange.ObjectReference,
x.ObjectChange.DisplayName,
PropertyChangeId=x.PropertyChange.Id,
x.PropertyChange.PropertyName,
x.PropertyChange.ChangeType,
x.PropertyChange.OriginalValue,
x.PropertyChange.Value
}).OrderByDescending(x=>x.ChangeSetId).ToList() //ChangeSetId=c.Id
To keep it simple imagine each table has a column named Description and we want to retrieve a flat result like this:
-------------------------------------------------------------------------------------
ChangeSetDescription | ObjectChangeDescription | PropertyChangeDescription |
-------------------------------------------------------------------------------------
Short Answer
You do not need to do a join because EF will figure out the joining for you based on your data model. You can just do this. Imagine ctx is an instance of a class which derives DbContext :
var query = ctx.ChangeSets
.SelectMany(x => x.ObjectChanges, (a, oc) => new
{
ChangeSetDescription = a.Description,
ObjectChangeDescription = oc.Description
,
Pcs = oc.PropertyChanges
})
.SelectMany(x => x.Pcs, (a, pc) => new
{
ChangeSetDescription = a.ChangeSetDescription,
ObjectChangeDescription = a.ObjectChangeDescription,
PropertyChangeDesription = pc.Description
});
var result = query.ToList();
Long Answer
Why do I not need the join?
When you create your EF model, regardless of whether you are using code first, database first or a hybrid approach, if you create the relationships correctly then EF will figure out the joins for you. In a database query, we need to tell the database engine how to join, but in EF we do not need to do this. EF will figure out the relationship based on your model and do the joins using the navigation properties. The only time we should be using joins is if we are joining on some arbitrary property. In other words, we should seldom use joins in EF queries.
Your EF queries should not be written as a direct translation of SQL queries. Take advantage of EF and let it do the work for you.
Can you explain the query in the short answer?
It is the SelectMany method which will throw readers off a little. But it is pretty easy. Let's use a simple class as example:
public class Student
{
public string Name { get; private set; }
public IEnumerable<string> Courses { get; private set; }
public Student(string name, params string[] courses)
{
this.Name = name;
this.Courses = courses;
}
}
Let's create 2 students and add them to a generic list:
var s1 = new Student("George", "Math");
var s2 = new Student("Jerry", "English", "History");
var students = new List<Student> { s1, s2 };
Now let's imagine we need to get the name of the students and the classes they are taking as flat result set. In other words, we do not want Jerry to have a list with 2 subjects but instead we want 2 records for Jerry. Naturally, we will think of SelectMany and do this:
var courses = students.SelectMany(x => new { Name = x.Name, Courses = x.Courses });
But that will not work and cause a compilation error:
The type arguments for method 'Enumerable.SelectMany<TSource, TResult>
(IEnumerable<TSource>, Func<TSource, IEnumerable<TResult>>)'
cannot be inferred >from the usage. Try specifying the type arguments explicitly.
The SelectMany method takes a Func which will return IEnumerable<TResult> NOT TResult so that will not work. Luckily, it has an overload that we can use. But the overload has a, well, intimidating signature:
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>
(
this IEnumerable<TSource> source,
Func<TSource, IEnumerable<TCollection>> collectionSelector,
Func<TSource, TCollection, TResult> resultSelector);
Basically what that is saying is the first Func should return an IEnumerable<TCollection> and the second will receive the TSource and TCollection as input and we can return a TResult as output. Ok so let's do that:
var courses = students.SelectMany(x => x.Courses,
(a, b) => new { Name = a.Name, Course = b });
So in the above line, a is the source which is our students list and b is the string yielded to us one by one from the first Func which we said will return x.Courses. The TResult we are returning is an anonymous type.
Hopefully this example will clarify what is going on in the short answer.
Or what you can do is, since your where expression is for changeSet table, you can filter it when you select from it within your linq
var foo = from c in _uow.GetRepository<ChangeSet>().Entities.Where(expression) //Notice lambda exp here
join o in _uow.GetRepository<ObjectChange>().Entities on c.Id equals o.ChangeSetId
join p in _uow.GetRepository<PropertyChange>().Entities on o.Id equals p.ObjectChangeId
select new
{
ChangeSetId = c.Id,
Timestamp = c.Timestamp,
User = c.User.DisplayName,
EntityType = o.TypeName,
EntityValue = o.DisplayName,
Property = p.PropertyName,
OldValue = p.OriginalValue,
NewValue = p.Value
};
I'm trying to create handler to save the result of LINQ query to list and that what i reached to
public static List<string> Get()
{
LesamisContainer LC = new LesamisContainer();
List<string> list = (from pb in LC.PayBills
join c in LC.Customers on pb.CustomerId equals c.Id
join d in LC.Departments on pb.DepartmentId equals d.Id
select new { pb.Id, c.FullName, d.Name, pb.Discount, pb.TotalAmount, pb.Details, pb.Date, pb.CustomerId, pb.DepartmentId } into x
select x).Tolist();
return list;
}
but i got this exception
http://i60.tinypic.com/2lxu2o.png
Cannnot convert type 'System.Collections.Generic.List' to 'System.Collections.Generic.List'
You're creating an anonymous type with
select new { pb.Id, c.FullName, d.Name, pb.Discount, pb.TotalAmount, pb.Details, pb.Date, pb.CustomerId, pb.DepartmentId } into x
The select x just returns this type. To get a List<string> you would need to return on of the properties from this instead, eg: select x.FullName
That would be a pretty odd way of doing things though, and probably not what you really intend. I assume you don't actually want a List<string>.
In which case, since you're returning the result from the method it should be declared and not anonymous:
public class PayBillModel
{
public int Id {get;set;}
...
}
then
select new PayBillModel() { pb.Id, c.FullName, d.Name, pb.Discount, pb.TotalAmount, pb.Details, pb.Date, pb.CustomerId, pb.DepartmentId }
This way, you end up with a List<PayBillModel>.
You're getting multiple types. Try changing the List type to object instead of string
List<object> list = (from pb in LC.PayBills
join c in LC.Customers on pb.CustomerId equals c.Id
join d in LC.Departments on pb.DepartmentId equals d.Id
select new { pb.Id, c.FullName, d.Name, pb.Discount, pb.TotalAmount, pb.Details, pb.Date, pb.CustomerId, pb.DepartmentId } into x
select x).Tolist();
You can then browse through the list by:
foreach (object item in list)
{
if (item is pb.Id)
{
//do something
}
//or
//if (item.GetType() == typeof(PayBills)) { }
}
I am trying to write a slightly complex LINQ to SQL query.
I have a table called
Fruit (FruitID, FieldOne, FieldTwo)
and
FruitChangeHistory (FruitChangeHIstoryID, FruitID, Date)
What I want to do is return the Fruit list, to the view. But the View model will contain an extra field, LastChangeDate. So like: FruitID, FieldOne, FieldTwo, LastChangedDate
I need to work out how to join on the fruitchangehistory with the fruitid, then sort the dates and return only the latest change date.
This is what I have so far:
var list = from p in EntityFramework.Fruits
join h on EntityFramework.FruitChangHistory
on p.FruitID equals h.FruitID
orderby h.LastChangedDate ascending
select new FruitVM
{
FruitID = p.FruitId,
FieldOne = p.FieldOne,
FieldTwo = p.FieldTwo,
LastChangedDate = h.Date
}
but not quite working as planned.
Anyone can help?
Something like this:
var list = from p in EntityFramework.Fruits
select new FruitVM
{
FruitID = p.FruitId,
FieldOne = p.FieldOne,
FieldTwo = p.FieldTwo,
LastChangedDate = (from h in EntityFramework.FruitChangHistory
where p.FruitID == h.FruitID
orderby h.LastChangedDate ascending
select h.LastChangedDate).FirstOrDefault()
}
It sounds like you are using Linq to Entities / Entity Framework and not Linq to Sql.
If your Fruits entity has a navigation property FruitChangeHistories (which it sounds like it should, sinceFruitChangeHistory has a FK to Fruit) you can do:
var list = from p in EntityFramework.Fruits
select new FruitVM()
{
FruitID = p.FruitId,
FieldOne = p.FieldOne,
FieldTwo = p.FieldTwo,
LastChangedDate = FruitChangeHistories.OrderByDescending( x => x.Date)
.FirstOrDefault()
}
This question is continuation from:
Can't think of query that solves this many to many
This LINQ query is recommended to me by user #juharr, i just added string concatenation in purpose of grouping first and last name into full name.
var courseViews = from c in db.Courses
select new CourseView()
{
CourseID = c.ID,
ProfessorName = (from l in c.Leturers
where l.Is_Professor
select l.LastName+" "+l.FirstName).FirstOrDefault(),
AssistantNames = (from l in c.Leturers
where !l.Is_Professor
select l.LastName+" "+l.FirstName)
.ToList() //hmmm problem
};
ModelView that i used is another possible cause of problems:
public class CourseView
{
public int ID { get; set; }
public string CourseName { get; set; }
public string ProfessorName { get; set; }
public List AssistantNames { get; set; }
}
Hmm List of strings for Assistant names problematic isn't it?
At the end of my stupidity, in View i looped through this list with #foreach(var s in item.AssistantNames){#s}
#Ladislav suggested using IQueryable instead of string,how where?
For solution that i made so far i get following error:
LINQ to Entities does not recognize the method 'System.Collections.Generic.List1[System.String] ToList[String](System.Collections.Generic.IEnumerable1[System.String])' method, and this method cannot be translated into a store expression.
Need help!
Drop the ToList() call and then change the Assistants property to this:
public IQueryable AssistantNames { get; set; }
See if this works
var courseViews = from c in db.Courses
let assistantsList = (from l in c.Leturers
where !l.Is_Professor
select l.LastName+" "+l.FirstName).ToList()
select new CourseView()
{
CourseID = c.ID,
ProfessorName = (from l in c.Leturers
where l.Is_Professor
select l.LastName+" "+l.FirstName).FirstOrDefault(),
AssistantNames = assistantsList
};
Another approach would be, since you are materializing all courses anyway to split the query in two parts, the first one materializes the data, the second one uses it to create the course views (this is now a Linq to Objects query):
var courses = (from c in db.Courses
select new { c.ID, Leturers = c.Leturers.ToList() }).ToList();
var courseViews = from c in courses
select new CourseView()
{
CourseID = c.ID,
ProfessorName = (from l in c.Leturers
where l.Is_Professor
select l.LastName+" "+l.FirstName).FirstOrDefault(),
AssistantNames = (from l in c.Leturers
where !l.Is_Professor
select l.LastName+" "+l.FirstName)
.ToList()
};
I'm new to Linq to Entity stuff, so I don't know if what I'm doing is the best approach.
When I do a query like this it compiles, but throws an error that it doesn't recognize the method GetItemSummaries. Looking it up, this seems to be because it doesn't like a custom method inside the query.
return (from c in _entity.Category
from i in c.Items
orderby c.Id, i.Id descending
select new CategoryDto
{
Id = c.Id,
Name = c.Name,
Items = GetItemSummaries(c)
}).ToList();
private IEnumerable<ItemSummary> GetItemSummaries(CategoryDto c)
{
return (from i in c.Items
select new ItemSummary
{
// Assignment stuff
}).ToList();
}
How would I combine this into a single query since I can't call a custom method?
I tried just replacing the method call with the actual query, but then that complains that ItemSummary isn't recognized instead of complaining that the method name isn't recognized. Is there any way to do this? (Or a better way?)
You should be able to do the following:
return (
from c in _entity.Category
orderby c.Id descending
select new CategoryDto
{
Id = c.Id,
Name = c.Name,
Items = (
from i in c.Items
order by i.Id descending
select new ItemSummary
{
// Assignment stuff
}
)
}).ToList();
It's just a matter of making sure that ItemSummary is public so that it's visible to the query.
If it's just a Dto though, you could use an anon type, eg:
from i in c.Items
order by i.Id descending
select new
{
Id = i.Id,
Name = i.Name
}
This creates a type with the 'Id' and 'Name' properties. All depends what your consuming code needs :)
Try making the GetItemSummaries method an extension method on the Category class
private static IEnumerable<ItemSummary> GetItemSummaries(this Category c)
and then call it with
select new Category
{
Id = c.Id,
Name = c.Name,
Items = c.GetItemSummaries()
}).ToList();
Marc