I am working on a Blazor Project and using Dapper to Pull Data from a SQL Db.
I am pulling 3 tables at the moment. An entity table, a specialty table and a bridge table that is there to maintain the many to many relationship between entity and specialties.
I am able to pull from SQL fine and I want to combine the data in my data service and inject it as a new object model to the Blazor component.
Here are the models:
Entity
public class EntityModel : IEntityModel
{
public int Id { get; set; }
public int PhysicianId { get; set; }
public int PartnerId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Specialty
public class SpecialtyModel : ISpecialtyModel
{
public int Id { get; set; }
public string Name { get; set; }
}
BridgeModel
public class BridgeModel : IBridgeModel
{
public int Id1 { get; set; }
public int Id2 { get; set; }
}
I made the properties in the bridge model generic so I could use it with another bridge table I have for a many to many relationship. The bridge tables are just two columns of IDs that link their respective tables. In this case Entity.Id and Specialty.Id
Here is the model I am combining all the information into:
public class CombinedModel : ICombinedModel
{
public int Id { get; set; }
public int PhysicianId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<ISpecialtyModel> Specialties { get; set; }
}
Here is the inside of my data service where I am stuck trying to combine the data with Linq and Lambda expressions.
public async Task<List<IEntityModel>> ReadEntities()
{
var entities = await _dataAccess.LoadData<EntityModel, dynamic>("dbo.spEntity_Read", new { }, "SqlDb");
return entities.ToList<IEntityModel>();
}
public async Task<List<ISpecialtyModel>> ReadSpecialties()
{
var specialties = await _dataAccess.LoadData<SpecialtyModel, dynamic>("dbo.spSpecialty_Read", new { }, "SqlDb");
return specialties.ToList<ISpecialtyModel>();
}
public async Task<List<IBridgeModel>> ReadEntitySpecialtyBridge()
{
var bridge = await _dataAccess.LoadData<BridgeModel, dynamic>("dbo.spEntitySpecialty_Read", new { }, "SqlDb");
return bridge.ToList<IBridgeModel>();
}
public async Task<List<ICombinedModel>> CombineData()
{
var entities = await ReadEntities();
var specialties = await ReadSpecialties();
var bridge = await ReadEntitySpecialtyBridge();
//var combined = (from e in entities
// join b in bridge on e.Id equals b.Id1
// join s in specialties on b.Id2 equals s.Id
// select new CombinedModel()
// {
// Id = e.Id,
// PhysicianId = e.PhysicianId,
// FirstName = e.FirstName,
// LastName = e.LastName,
// Specialties = new List<ISpecialtyModel>()
// });
var combined = (from e in entities
select new CombinedModel
{
Id = e.Id,
PhysicianId = e.PhysicianId,
FirstName = e.FirstName,
LastName = e.LastName,
Specialties = specialties.Where(s => )
}
);
return combined.ToList<ICombinedModel>();
This is where I am stuck. How can I write this Linq query to combine this data into the new model?
I am able to get data passed into the razor component but I am not combining it correctly and this is where I am stuck.
I hope someone can shed some light on the matter. Thank you for taking the time to look over this, I appreciate it.
With Thanks,
Cesar
If you wanted to process locally (doing on the server should eliminate the need to pull bridge over from the database, and if bridge contains records that aren't relevant to entities could potentially be a lot of unnecessary data traffic) then you just need to filter specialties by the correct bridge records for a given entity:
var combined = (from e in entities
select new CombinedModel {
Id = e.Id,
PhysicianId = e.PhysicianId,
FirstName = e.FirstName,
LastName = e.LastName,
Specialties = specialties.Where(s => bridge.Where(b => b.Id1 == e.Id).Select(b => b.Id2).Contains(s.Id)).ToList()
});
Depending on the size of specialties and entities, it might be worthwhile to pre-process bridge to make access for a given entity more efficient (Where is O(n) so specialties.Where x bridge.Where is O(n*m)):
var bridgeDict = bridge.GroupBy(b => b.Id1).ToDictionary(bg => bg.Key, bg => bg.Select(b => b.Id2).ToHashSet());
var combined = (from e in entities
let eBridge = bridgeDict[e.Id]
select new CombinedModel {
Id = e.Id,
PhysicianId = e.PhysicianId,
FirstName = e.FirstName,
LastName = e.LastName,
Specialties = specialties.Where(s => eBridge.Contains(s.Id)).ToList()
});
Related
I have 2 queries as follows:
var q1 = await context.Submissions
.Include(s => s.Application)
.ToListAsync();
// q1 is of type List<Submissions>
var q2 = await context.Applications
.Select(a => new Application
{
Id = a.Id,
Member = a.Histories.OrderByDescending(h => h.ModifiedDate).FirstOrDefault().Member
}).ToListAsync();
// q2 is of type List<Applications>
Is there a way to combine these 2 queries and have the type as List<Submissions>?
Note: I'm using EF Core version 3
Submissions class:
public class Submission
{
public Guid Id { get; set; }
public string Name { get; set; }
public Application Application { get; set; }
public Guid? ApplicationId { get; set; }
}
Applications class:
public class Application
{
public Guid Id { get; set; }
public string Member { get; set; }
public ICollection<History> Histories { get { return _Histories; } set { _Histories = value; _currentMember =null; } }
private ICollection<History> _memberHistories;
private MemberHistory _currentMember = null;
}
There are .Include() and .ThenInclude()
var q1 = context.Submission
.Include(submission => submission.Application)
.ThenInclude(application = > application.Histories);
having too many includes can give performance issues, unless you actually start splitting up the query. Another approach would be to contain it in a select statement which often performs better. but gives sort of a split result.
var q2 = context.Submissions.Select(submission => new
{
SubMission = submission
Application = submission.Application
});
var result = q2.ToList().Select(t => t.Submission);
due to the built in EF Core mapper, the relations are handled for you so application are loaded and "attached" correctly to the Submission set on the result.
I'm trying to query something from an indirectly related entity into a single-purpose view model. Here's a repro of my entities:
public class Team {
[Key]
public int Id { get; set; }
public string Name { get; set; }
public List<Member> Members { get; set; }
}
public class Member {
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
public class Pet {
[Key]
public int Id { get; set; }
public string Name { get; set; }
public Member Member { get; set; }
}
Each class is in a DbSet<T> in my database context.
This is the view model I want to construct from a query:
public class PetViewModel {
public string Name { get; set; }
public string TeamItIndirectlyBelongsTo { get; set; }
}
I do so with this query:
public PetViewModel[] QueryPetViewModel_1(string pattern) {
using (var context = new MyDbContext(connectionString)) {
return context.Pets
.Where(p => p.Name.Contains(pattern))
.ToArray()
.Select(p => new PetViewModel {
Name = p.Name,
TeamItIndirectlyBelongsTo = "TODO",
})
.ToArray();
}
}
But obviously there's still a "TODO" in there.
Gotcha: I can not change the entities at this moment, so I can't just include a List<Pet> property or a Team property on Member to help out. I want to fix things inside the query at the moment.
Here's my current solution:
public PetViewModel[] QueryPetViewModel_2(string pattern) {
using (var context = new MyDbContext(connectionString)) {
var petInfos = context.Pets
.Where(p => p.Name.Contains(pattern))
.Join(context.Members,
p => p.Member.Id,
m => m.Id,
(p, m) => new { Pet = p, Member = m }
)
.ToArray();
var result = new List<PetViewModel>();
foreach (var info in petInfos) {
var team = context.Teams
.SingleOrDefault(t => t.Members.Any(m => m.Id == info.Member.Id));
result.Add(new PetViewModel {
Name = info.Pet.Name,
TeamItIndirectlyBelongsTo = team?.Name,
});
}
return result.ToArray();
}
}
However, this has a "SELECT N+1" issue in there.
Is there a way to create just one EF query to get the desired result, without changing the entities?
PS. If you prefer a "plug and play" repro containing the above, see this gist.
You've made the things quite harder by not providing the necessary navigation properties, which as #Evk mentioned in the comments do not affect your database structure, but allow EF to supply the necessary joins when you write something like pet.Member.Team.Name (what you need here).
The additional problem with your model is that you don't have a navigation path neither from Team to Pet nor from Pet to Team since the "joining" entity Member has no navigation properties.
Still it's possible to get the information needed with a single query in some not so intuitive way by using the existing navigation properties and unusual join operator like this:
var result = (
from team in context.Teams
from member in team.Members
join pet in context.Pets on member.Id equals pet.Member.Id
where pet.Name.Contains(pattern)
select new PetViewModel
{
Name = pet.Name,
TeamItIndirectlyBelongsTo = team.Name
}).ToArray();
I have these 3 classes:
Employee
Student
Person
Code:
public class Employee
{
public Guid Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public string Gender { get; set; }
public long TimeStamp { get; set; }
}
public class Student
{
public Guid Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public long TimeStamp { get; set; }
}
public class Person<br>
{
public string Name { get; set; }
public int Age { get; set; }
}
I create 4 Lists :
var studentList = new List<Student>();// fill the List with a lot of Stundents
var employeeList = new List<Student>(); // fill the List with a lot of employees
var personList1 = new List<Person>();
var personList2 = new List<Person>();
Select all students and employees
var allStudents = studentList.Select(a => a); // does not make a lot of sence but for testing
var allEmployee = employeeList.Select(b => b);
I want to map allStudents to
personList1.AddRange(allStudents.Select(a => new Person()
{
Age = a.Age,
Name = a.Name
} ));
I want to get all Employees where the value of TimeStape is not mentioned in the allStundent List
var allEmployeesWithDifferentTimeStampThanStundent =
allEmployee.Where(a => !allStudents.Select(b =>b.TimeStamp).Contains(a.TimeStamp));
mapping again
personList2.AddRange(allEmployeesWithDifferentTimeStampThanStundent.Select
(a => new Person()
{
Age = a.Age,
Name = a.Name
} ));
merge both lists
personList1.AddRange(personList2);
Is there a better and more efficient way to do this?
The personList2 variable appears only to be there as an intermediate for projecting to the Person type -- if that's the case, you could skip its creation and use query syntax like so:
var personsFromNonMatchingEmployees =
from employee in allEmployee
join student in allStudents
on employee.TimeStamp equals student.TimeStamp into studentsWithMatchingTimeStamp
where !studentsWithMatchingTimeStamp.Any()
select new Person { Age = employee.Age, Name = employee.Name };
personList1.AddRange(personsFromNonMatchingEmployees);
This is similar to the other GroupJoin approach since the compiler translates the above into a GroupJoin call. The use of join/group-join necessarily performs better than the Where..Contains approach since it makes use of hashing - in other words, it's an algorithmic Big-O improvement that should be quite noticeable for any more than a few Student or Employee instances.
By selecting the new Person object in the query, I'm able to bypass the personList2 list altogether. I find that I'm almost always able to eliminate temporary lists by doing selects like this that project to the type that I'm really interested in. I also left out the () on the new Person { .. } since the compiler doesn't require it.
Shy of changing up the inheritance and making Employee : Person & Student : Person, I don't think there's much more to improve.
You can use GroupJoin to find all employees without a matching Student record with the same timestamp:
var employeesDiffTS = allEmployee
.GroupJoin(allStudents, e => e.TimeStamp, s => s.TimeStamp, (e, students) => new { Emp = e, HasMatch = students.Any() })
.Where(em => !em.HasMatch)
.Select(em => em.Emp)
personList2.AddRange(employeeDiffTS.Select(a => new Person { Age = a.Age, Name = a.Name }));
personList1.AddRange(personList2);
I have a database similar to below:
Order
===============
OrderID
Description
EmployeeID
...other fields
Product
===============
ProductID
...other fields
OrderProducts
===============
OrderID
ProductID
Employee
===============
EmployeeID
...other fields
I'm using Linq to Entities and the edmx file has been created without the OrderProducts table, as it is just a joining table. The Products table is a list of static products - I dont need to insert any rows at the moment. The Order table I can insert rows successfully using the following code:
[Serializable]
public class MyOrderObject
{
public int OrderID { get; set; }
public string OrderDescription { get; set; }
public int? EmployeeID { get; set; }
public IEnumerable<MyProductObject> ProductsList { get; set; }
...other fields
}
[Serializable]
public class MyProductObject
{
public int ProductID { get; set; }
...other fields
}
private static void AddNewOrder(MyOrderObject order)
{
using (var context = DatabaseHelper.CreateContext())
{
var dbOrder = new Order
{
OrderID = order.OrderID,
Description = order.OrderDescription,
Employee = context.Employees.SingleOrDefault(x => x.EmployeeID == order.EmployeeID),
}
context.AddToOrders(dbOrder);
context.SaveChanges();
}
}
How do I insert into the database my list of child relationship records??
I've tried:
List<int> ProductIDs = order.ProductsList.Select(x => x.ProductID).ToList();
//dbOrder.Products.Attach(context.Products.Where(x => ProductIDs.Contains(x.ProductID)));
//or dbOrder.Products = context.Products.Where(x => ProductIDs.Contains(x.ProductID));
//or dbOrder.Products = context.Products.Contains(ProductIDs);
//or foreach(var p in order.ProductsList)
// {
// context.AttachTo("Products", new Product { ProductID = p.ProductID });
// }
You need to change the ProductsList type to ICollection. And make sure it is part of the EDMX mapped model.
public class MyOrderObject
{
public int OrderID { get; set; }
public string OrderDescription { get; set; }
public int? EmployeeID { get; set; }
public ICollection<MyProductObject> ProductsList { get; set; }
...other fields
}
Then you can add products
var products = context.Products.Where(/**/);
foreach(var p in products)
order.ProductsList.Add(p);
No need to change the object type:
var dbOrder = new Order
{
OrderID = order.OrderID,
Description = order.OrderDescription,
Employee = context.Employees.SingleOrDefault(x => x.EmployeeID == order.EmployeeID),
}
//this adds the relationship to the child without adding a new child record - perfect!
foreach(var p in order.ProductsList)
{
dbOrder.Products.Add(p);
}
context.AddToOrders(dbOrder);
context.SaveChanges();
I'm still learning Entity Framework and Linq-To-Entities, and I was wondering if a statement of this kind is possible:
using (var context = new MyEntities())
{
return (
from a in context.ModelSetA.Include("ModelB")
join c in context.ModelSetC on a.Id equals c.Id
join d in context.ModelSetD on a.Id equals d.Id
select new MyModelA()
{
Id = a.Id,
Name = a.Name,
ModelB = new MyModelB() { Id = a.ModelB.Id, Name = a.ModelB..Name },
ModelC = new MyModelC() { Id = c.Id, Name = c.Name },
ModelD = new MyModelD() { Id = d.Id, Name = d.Name }
}).FirstOrDefault();
}
I have to work with a pre-existing database structure, which is not very optimized, so I am unable to generate EF models without a lot of extra work. I thought it would be easy to simply create my own Models and map the data to them, but I keep getting the following error:
Unable to create a constant value of type 'MyNamespace.MyModelB'. Only
primitive types ('such as Int32, String, and Guid') are supported in
this context.
If I remove the mapping for ModelB, ModelC, and ModelD it runs correctly. Am I unable to create new nested classes with Linq-To-Entities? Or am I just writing this the wrong way?
What you have will work fine with POCOs (e.g., view models). Here's an example. You just can't construct entities this way.
Also, join is generally inappropriate for a L2E query. Use the entity navigation properties instead.
I have created your model (how I understand it) with EF 4.1 in a console application:
If you want to test it, add reference to EntityFramework.dll and paste the following into Program.cs (EF 4.1 creates DB automatically if you have SQL Server Express installed):
using System.Linq;
using System.Data.Entity;
namespace EFNestedProjection
{
// Entities
public class ModelA
{
public int Id { get; set; }
public string Name { get; set; }
public ModelB ModelB { get; set; }
}
public class ModelB
{
public int Id { get; set; }
public string Name { get; set; }
}
public class ModelC
{
public int Id { get; set; }
public string Name { get; set; }
}
public class ModelD
{
public int Id { get; set; }
public string Name { get; set; }
}
// Context
public class MyContext : DbContext
{
public DbSet<ModelA> ModelSetA { get; set; }
public DbSet<ModelB> ModelSetB { get; set; }
public DbSet<ModelC> ModelSetC { get; set; }
public DbSet<ModelD> ModelSetD { get; set; }
}
// ViewModels for projections, not entities
public class MyModelA
{
public int Id { get; set; }
public string Name { get; set; }
public MyModelB ModelB { get; set; }
public MyModelC ModelC { get; set; }
public MyModelD ModelD { get; set; }
}
public class MyModelB
{
public int Id { get; set; }
public string Name { get; set; }
}
public class MyModelC
{
public int Id { get; set; }
public string Name { get; set; }
}
public class MyModelD
{
public int Id { get; set; }
public string Name { get; set; }
}
class Program
{
static void Main(string[] args)
{
// Create some entities in DB
using (var ctx = new MyContext())
{
var modelA = new ModelA { Name = "ModelA" };
var modelB = new ModelB { Name = "ModelB" };
var modelC = new ModelC { Name = "ModelC" };
var modelD = new ModelD { Name = "ModelD" };
modelA.ModelB = modelB;
ctx.ModelSetA.Add(modelA);
ctx.ModelSetB.Add(modelB);
ctx.ModelSetC.Add(modelC);
ctx.ModelSetD.Add(modelD);
ctx.SaveChanges();
}
// Run query
using (var ctx = new MyContext())
{
var result = (
from a in ctx.ModelSetA.Include("ModelB")
join c in ctx.ModelSetC on a.Id equals c.Id
join d in ctx.ModelSetD on a.Id equals d.Id
select new MyModelA()
{
Id = a.Id,
Name = a.Name,
ModelB = new MyModelB() {
Id = a.ModelB.Id, Name = a.ModelB.Name },
ModelC = new MyModelC() {
Id = c.Id, Name = c.Name },
ModelD = new MyModelD() {
Id = d.Id, Name = d.Name }
}).FirstOrDefault();
// No exception here
}
}
}
}
This works without problems. (I have also recreated the model from the database (which EF 4.1 had created) in EF 4.0: It works as well. Not surprising since EF 4.1 doesn't change anything in LINQ to Entities.)
Now the question is why you get an exception? My guess is that there is some important difference in your Models or ViewModels or your query compared to the simple model above which is not visible in your code example in the question.
But the general result is: Projections into nested (non-entity) classes work. (I'm using it in many situations, even with nested collections.) Answer to your question title is: Yes.
What Craig posted does not seem to work for nested entities. Craig, if I am misunderstood what you posted, please correct me.
Here is the workaround I came up with that does work:
using (var context = new MyEntities())
{
var x = (
from a in context.ModelSetA.Include("ModelB")
join c in context.ModelSetC on a.Id equals c.Id
join d in context.ModelSetD on a.Id equals d.Id
select new { a, b, c }).FirstOrDefault();
if (x == null)
return null;
return new MyModelA()
{
Id = x.a.Id,
Name = x.a.Name,
ModelB = new MyModelB() { Id = x.a.ModelB.Id, Name = x.a.ModelB..Name },
ModelC = new MyModelC() { Id = x.c.Id, Name = x.c.Name },
ModelD = new MyModelD() { Id = x.d.Id, Name = x.d.Name }
};
}
Since Entity Framework can't handle creating nested classes from within the query, I simply returned an anonymous object from my query containing the data I wanted, then mapped it to the Model