I have a simple structure like this
class Host{
Node RootNode;
}
class Node{
Node Parent;
Children ICollection<Node>Children;
}
And I have the model builder as
modelBuilder.Entity<Node>()
.HasOptional(x => x.Parent)
.WithMany(y => y.Children)
.Map(m => m.MapKey("Parent_Id")).WillCascadeOnDelete(false);
However, when I query I do not get the whole tree but just the first child. How can I get the whole tree data loaded. Here is the query.
return db.Host
.Include(l => l.RootNode)
Related
How to user Map method in Entity Framework Core 6. After upgrading to Entity Framework Core 6 the Map() method no longer works. Is there something similar I can use to Map the columns to a table?
Example of my code below.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var relationship in modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetForeignKeys()))
{
relationship.DeleteBehavior = DeleteBehavior.Restrict;
}
modelBuilder.Entity<RoleGroup>()
.HasMany(c => c.Roles).WithMany(i => i.RoleGroups).Map(t => t.MapLeftKey("RoleGroupId")
.MapRightKey("RoleId")
.ToTable("RoleGroupRole"));
}
Most examples for EF Core have a RoleGroupRole entity defined, but they do support using a Dictionary<string, object> for a shadow entity placeholder for basic joining tables:
modelBuilder.Entity<RoleGroup>
.HasMany(u => u.Roles)
.WithMany(g => g.RoleGroups)
.UsingEntity<Dictionary<string, object>>(
right => right
.HasOne<Role>()
.WithMany(),
left => left
.HasOne<RoleGroup>()
.WithMany(),
join => join
.ToTable("RoleGroupRoles"));
The gotcha with this configuration is that the expressions for the two sides goes "Right" then "Left", it's easy to get them backwards. :)
In my design, I have a Challenge aggregate root that maintains a list of strategies that collectively determine if the challenge is complete.
There are several types of strategies that examine the challenge submission in their own way. Each type of strategy inherits from the base ChallengeFullfilmentStrategy, as shown in the diagram.
When a submission is made, I load the challenge along with its strategies with the following code:
return _dbContext.Challenges
.Where(c => c.Id == challengeId)
.Include(c => c.Solution)
.Include(c => c.FulfillmentStrategies)
.ThenInclude(s => (s as BasicMetricChecker).ClassMetricRules)
.ThenInclude(r => r.Hint)
.Include(c => c.FulfillmentStrategies)
.ThenInclude(s => (s as BasicMetricChecker).MethodMetricRules)
.ThenInclude(r => r.Hint)
.Include(c => c.FulfillmentStrategies)
.ThenInclude(s => (s as BasicNameChecker).Hint)
.FirstOrDefault();
This setup is a consequence of the polymorphism introduced by the strategy hierarchy. Recently, I've tried to add a more sophisticated strategy (ProjectChecker, marked in red on the diagram). Through an intermediate class, it introduces a composite relationship, where now a Strategy has a list of Strategies (through the SnippetStrategies class).
This change severely complicates the data model, as now the code should look something like this:
return _dbContext.Challenges
.Where(c => c.Id == challengeId)
.Include(c => c.Solution)
.Include(c => c.FulfillmentStrategies)
.ThenInclude(s => (s as BasicMetricChecker).ClassMetricRules)
.ThenInclude(r => r.Hint)
.Include(c => c.FulfillmentStrategies)
.ThenInclude(s => (s as BasicMetricChecker).MethodMetricRules)
.ThenInclude(r => r.Hint)
.Include(c => c.FulfillmentStrategies)
.ThenInclude(s => (s as BasicNameChecker).Hint)
.Include(c => c.FulfillmentStrategies)
.ThenInclude(s => (s as ProjectChecker).SnippetStrategies)
.ThenInclude(snippetStrats => snippetStrats.Strategies)
.ThenInclude(s => (s as BasicMetricChecker).MethodMetricRules)
//More code here to include the other children, not sure if this can even work.
.FirstOrDefault();
I'm not sure if I've hit a limitation of EF Core or if I'm not aware of some mechanism that can solve this issue.
If it's the former, I was considering serializing my Challenge aggregate into a JSON object (I'm using postgresql) and removing this part of the relationship model. While it makes sense from a domain perspective (I either need only the challenge header or all of it - including all the strategies etc.), my research so far has revealed that System.Text.Json suffers from the same limitation and that I'll need to write a custom JSONConverter to enable serialization of this data structure.
The cleanest option I've found so far is the use of Newtonsoft along with the JsonSubtypes library, but I wanted to check if I'm missing something that would help solve my issue without introducing these dependencies (especially since JsonSubtypes seems to be less active).
This is a static helper method I created for when I had the same problem, utilizing reflection.
Imagine it like a beefed up .Find(). It loops through all the properties of a class and loads them in the same fashion as you would with the .Include() and .ThenInclude() pattern that you follow in your code.
This does have an extra content in the first if/else block where it finds the DbContext Model from an Interface being passed in for the Type parameter. As in our case we used quite a few interfaces to classify our Model classes (figured it could be useful). This has some hefty overhead, so be careful. We use the FullFindAsync for very specific use cases, and typically try to do .Includes().
You could also add further depth searching to the foreach loop at the end if you want to dive deeper in what gets loaded. Again, this has some hefty overhead as well, so make sure to test it on large datasets.
public static async Task<object> FullFindAsync(this DbContext context, Type entityType, params object[] keyValues)
{
object entity = null;
//handle an Interface as a passed in type
if (entityType.IsInterface)
{
//get all Types of Models in the DbContext that implement the Interface
var implementedModels = context.Model.GetEntityTypes()
.Select(et => et.ClrType)
.Where(t => entityType.IsAssignableFrom(t))
.ToList();
//loop through all Models and try to find the object via the parameters
foreach (var modelType in implementedModels)
{
entity = await context.FindAsync(modelType, keyValues);
if (entity != null) break;
}
}
else
//find the object, via regular Find
entity = await context.FindAsync(entityType, keyValues);
//didnt find the object
if (entity == null) return null;
//loop through all navigation properties
foreach (var navigation in entity.GetType().GetProperties())
{
//skip NotMapped properties (to avoid context collection error)
if (context.Entry(entity).Metadata.FindNavigation(navigation.Name) == null)
continue;
//check and load reference and collection properties
if (typeof(IDatabaseObject).IsAssignableFrom(navigation.PropertyType))
context.Entry(entity).Reference(navigation.Name).Load();
if (typeof(IEnumerable<object>).IsAssignableFrom(navigation.PropertyType))
context.Entry(entity).Collection(navigation.Name).Load();
}
return entity;
}
//***USAGES***
var fullModel = await _dbContext.FullFindAsync(typeof(MyModelObject), model.Id);
var correctModel = await _dbContext.FullFindAsync(typeof(IModelInterface), someRandomId);
we are using Entity Framework 5.0 and have a use case for creating a preview.
So I though I'd see if I could use EF to build my preview in EF data model classes and not persist (no SaveChanges).
(I could of course do this without EF, but I'd thought I'd experiment.)
Now I see some strange things when adding to my context.
In the code snippet below, the Add calls do not effect the collections added to (the DbSets). before = after = 0.
When I do Child.Add, the relation between parent and child is set (parent.Child now contains child). It did not do that before calling Add.
private void Insert(Parent parent, Child child)
{
// Context.Parent does not get any items:
int before = this.Context.Parent.Count();
this.Context.Parent.Add(parent);
int after = this.Context.Parent.Count();
// This affects Parent.Child:
this.Context.Child.Add(child);
}
Here is the vital part from the Context class (removed a lot for clarity):
modelBuilder.Entity<Child>(entity =>
{
// Removed code here...
entity.HasOne(d => d.Parent)
.WithMany(p => p.Child)
.HasForeignKey(d => d.ChildId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Child_Parent");
});
So what does Add really do?
I have six classes as shown below -
Class A{
int ValA;
ICollection<B> AllBs;
}
Class B{
int ValB;
ICollection<C> AllCs;
}
Class C{
int ValC;
ICollection<D> AllDs;
}
Class D{
int ValD;
ICollection<E> AllEs;
}
Class E{
int ValE;
ICollection<F> AllFs;
}
Class F{
int ValF;
}
I have to eager load an A entity with specific id and I want the result must load all the nested collections eagerly.
I wrote the following code to achieve the same -
var entity = _context.A.Where(a => a.Id == 1)
.Include(a => a.AllBs
.Select(b => b.AllCs
.Select(c => c.AllDs
.Select(d => d.AllEs
.Select(e => e.AllFs)))))
.FirstOrDefault();
Have I written the include statements correctly ? All the nested collections are not getting loaded properly. They are getting loaded upto AllBs, AllCs and further collections are not loaded.
Kindly help.
If this is EF Core you will want to use
_context.A
.Include(a => a.AllBs)
.ThenInclude(b => b.AllCs)
.ThenInclude(c => c.AllDs)
.ThenInclude(d => d.AllEs)
.ThenInclude(e => e.AllFs)
.Single(a => a.Id == 1)
Intellisense may give you a bit of grief diving through collections but it does work through.
For EF6, your statement should work, so if not I would look specifically at your mappings to ensure your FKs are resolving correctly. I'd also remove the .Where() & .FirstOrDefault() and instead use a .Single() at the end. It should not matter in the query generation, but Include statements are best placed before any filtering. Use an SQL Profiler to capture the SQL being sent to the server to see what tables are being requested and how they are joining. This might highlight some FK resolution errors.
How do I setup the mapping for these table relationships using code first and the fluent api?
I have a poco for all three entities created but most people don't have a poco for the "intermediate" table (PlanUser) so I couldn't find a good example to go off of. I need to tell EF to map the PlanUser.PlanCustomerId column to Plan.CustomerId but it either doesn't return the right results OR as in the current setup the mapping for Plan throws the error:
"The specified association foreign key columns 'CustomerId' are
invalid. The number of columns specified must match the number of
primary key columns."
public class PlanUserMap : EntityTypeConfiguration<PlanUser>
{
public PlanUserMap()
{
this.ToTable("PlanUser");
this.HasKey(c => new { c.CustomerId, c.PlanCustomerId });
this.HasRequired(c => c.Plan).WithMany().HasForeignKey(x => x.CustomerId).WillCascadeOnDelete(false);
this.HasRequired(c => c.Customer).WithMany().HasForeignKey(x => x.CustomerId).WillCascadeOnDelete(false);
}
}
public class PlanMap : EntityTypeConfiguration<Plan>
{
public PlanMap()
{
this.ToTable("Plan");
this.HasKey(c => c.CustomerId);
// the below line returns only 1 row for any given customer even if there are multiple PlanUser rows for that customer
//this.HasMany(c => c.PlanUsers).WithRequired().HasForeignKey(c => c.PlanCustomerId);
// this throws an error
this.HasMany(c => c.PlanUsers).WithMany().Map(m => m.MapLeftKey("PlanCustomerId").MapRightKey("CustomerId"));
}
}
public partial class CustomerMap : EntityTypeConfiguration<Customer>
{
public CustomerMap()
{
this.ToTable("Customer");
this.HasKey(c => c.Id);
this.HasMany(c => c.PlanUsers).WithRequired().HasForeignKey(c => c.CustomerId);
}
}
#Slauma, sql profiler shows these queries being executed. The second one should include customer id 43 in addition to customer id 1 but it does not. I don't know why its not retrieving that second row.
exec sp_executesql N'SELECT
[Extent1].[CustomerId] AS [CustomerId],
[Extent1].[PlanCustomerId] AS [PlanCustomerId],
[Extent1].[CreatedOnUtc] AS [CreatedOnUtc],
[Extent1].[IsSelected] AS [IsSelected],
[Extent1].[IsDeleted] AS [IsDeleted],
[Extent1].[AccessRights] AS [AccessRights]
FROM [dbo].[PlanUser] AS [Extent1]
WHERE [Extent1].[CustomerId] = #EntityKeyValue1',N'#EntityKeyValue1 int',#EntityKeyValue1=43
exec sp_executesql N'SELECT
[Extent1].[CustomerId] AS [CustomerId],
[Extent1].[Name] AS [Name],
[Extent1].[PlanTypeId] AS [PlanTypeId],
[Extent1].[OrderId] AS [OrderId],
[Extent1].[CreatedOnUtc] AS [CreatedOnUtc],
[Extent1].[IsActive] AS [IsActive]
FROM [dbo].[Plan] AS [Extent1]
WHERE [Extent1].[CustomerId] = #EntityKeyValue1',N'#EntityKeyValue1 int',#EntityKeyValue1=1
Here is the C# code that causes the queries to execute:
public List<Plan> GetPlans()
{
List<Plan> plans = new List<Plan>();
// add each plan they have access rights to to the list
foreach (var accessiblePlan in Customer.PlanUsers)
{
plans.Add(accessiblePlan.Plan);
}
return plans;
}
I think what you need is actually simpler than the mapping you tried:
public class PlanUserMap : EntityTypeConfiguration<PlanUser>
{
public PlanUserMap()
{
this.ToTable("PlanUser");
this.HasKey(pu => new { pu.CustomerId, pu.PlanCustomerId });
this.HasRequired(pu => pu.Customer)
.WithMany(c => c.PlanUsers)
.HasForeignKey(pu => pu.CustomerId)
.WillCascadeOnDelete(false);
this.HasRequired(pu => pu.Plan)
.WithMany(p => p.PlanUsers)
.HasForeignKey(pu => pu.PlanCustomerId)
.WillCascadeOnDelete(false);
}
}
public class PlanMap : EntityTypeConfiguration<Plan>
{
public PlanMap()
{
this.ToTable("Plan");
this.HasKey(p => p.CustomerId);
}
}
public partial class CustomerMap : EntityTypeConfiguration<Customer>
{
public CustomerMap()
{
this.ToTable("Customer");
this.HasKey(c => c.Id);
}
}
I am not sure why you disable cascading delete. I probably wouldn't do this (i.e. I would remove the WillCascadeOnDelete(false) for both relationships) because the association entity PlanUser is kind of dependent of the other two entities.
Here are a bit more details about this kind of model (sometimes called "many-to-many relationship with payload"): Create code first, many to many, with additional fields in association table
Ok, I figured it out. Thanks to #Slauma for the help! We are using the repository pattern with IOC(autofac). I shouldn't be calling Customer.PlanUsers in my repository service in the first place. Somewhere along the line we started resolving the Customer entity using IOC in the repository service to get at certain customer properties quickly and put it in a read-only variable. Inevitably this hosed us because instead of querying the entity repositories themselves we started with the Customer entity expecting it to be filled with everything we needed and its not pre-filled.
public List<Plan> GetPlans()
{
List<Plan> plans = new List<Plan>();
var list = _planUserRepository.Table.Where(a => a.CustomerId == Customer.Id).ToList();
foreach (var planUser in list)
{
plans.Add(planUser.Plan);
}
}