Entity Framework not generating INNER JOIN with .Include - entity-framework

I have a project where an .Include does not generate an INNER JOIN, while the INNER JOIN is generated in other projects using the same DbContext and code.
I am using EF Core Code-First. The DbContext is shared with other projects through NuGet.
Below are the two classes in question and their configuration in the DbContext:
[Table("Project", Schema = "ProjectManagement")]
public class AxosoftProject
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public int AxosoftId { get; set; }
public int? AxosoftParentId { get; set; }
public AxosoftProject AxosoftParent { get; set; }
public List<AxosoftProject> Children { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
public string Description { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public DateTime UpdateDate { get; set; }
public ICollection<AxosoftItem> Items { get; set; }
}
[Table("Item", Schema = "ProjectManagement")]
public class AxosoftItem
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public int AxosoftId { get; set; }
public int AxosoftProjectId { get; set; }
public AxosoftProject AxosoftProject { get; set; }
public string Name { get; set; }
public string ItemType { get; set; }
public string SubItemType { get; set; }
public decimal PercentComplete { get; set; }
public DateTime UpdateDate { get; set; }
}
DbContext:
modelBuilder.Entity<AxosoftProject>().HasKey(x => x.AxosoftId);
modelBuilder.Entity<AxosoftProject>().HasOne(x => x.AxosoftParent).WithMany(x => x.Children);
modelBuilder.Entity<AxosoftProject>().HasMany(x => x.Items);
modelBuilder.Entity<AxosoftItem>().HasKey(x => x.AxosoftId);
modelBuilder.Entity<AxosoftItem>().HasOne(x => x.AxosoftProject);
Code to query all projects and their items:
IMyEntities MyEntities = new MyEntities("Server=localhost;Database=MyEntities;Trusted_Connection=True;MultipleActiveResultSets=true");
var projects = MyEntities.AxosoftProjects.Include(x => x.Items).ToList();
var listwithitems = projects.Where(x => x.Items != null).ToList();
Depending on the project that I execute above code in, it will either do an INNER JOIN or no INNER JOIN at all. I tried deleting the whole project where above code does not perform an INNER JOIN, cloning it from the repository afterwards to start from fresh, still no INNER JOIN.
How do I go about debugging this further? I suspect the project where it doesn't work may be using an old version of the DLL's even if it looks like it's pointing to the correct ones.
UPDATE
To verify that the include is indeed not working, I did an additional test. If I load the AxosoftItems into the DbContext seperately, then EF will populate the Items collections for each AxosoftProject. Example:
var projects = MyEntities.AxosoftProjects.Include(x => x.Items).ToList();
var listwithitems1 = projects.Where(x => x.Items != null).ToList(); // 0 items
var items = MyEntities.AxosoftItems.ToList();
var listwithitems2 = projects.Where(x => x.Items != null).ToList(); // 41 items
UPDATE 2
The code and reference to EF Core was placed in an ASP.NET MVC project. Moving the code to a separate class library "fixes" the issue, the INNER JOIN is not ignored, and the code is exactly the same. I intend to download the source code and debug my way through, to figure out why the Include is being ignored in the MVC project.

Related

Define circular relationship for ICollection of bookings in entity framework

I am trying to add a circular relationship in my project. I have the following problem:
My database consists of a table with bookings (on a specific machine). Since the machines can handle multiple bookings at once, I have another table that stores all the (overlapping) parallel bookings. How can I now attach the overlapping bookings to the original booking element? I would like to access the overlaps like this:
var bookings = dbContext.Booking.Include(x => x.OverlapBookings).ToList();
foreach (var booking in bookings)
{
var overlaps = booking.OverlapBookings;
...
However, when trying to add the migration, I am running into the following error:
Unable to determine the relationship represented by navigation 'BookingDbModel.OverlapBookings' of type 'ICollection'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
How can I now define this circular relationship?
Here are the classes:
public class BookingDbModel
{
public int id { get; set; }
public string Name { get; set; }
public string Client { get; set; }
public string Machine { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public ICollection<OverlapBookingDbModel> OverlapBookings { get; set; }
}
and
public class OverlapBookingDbModel
{
public int OriginalBookingId { get; set; }
public BookingDbModel OriginalBooking { get; set; }
public int TargetBookingId { get; set; }
public BookingDbModel TargetBooking { get; set; }
}
With the following manual relationship definition, the entity updated successfully and all the models are now accessible with only one dbContext call:
DbContext.cs
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<BookingDbModel> Booking { get; set; }
public DbSet<OverlapBookingDbModel> OverlapBooking { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<OverlapBookingDbModel>()
.HasOne(p => p.OriginalBooking)
.WithMany(b => b.OverlapBookings)
.HasForeignKey(k => k.OriginalBookingId);
base.OnModelCreating(modelBuilder);
}
}
I can now access all related Overlapbookings like this:
var testbookings = dbContext.Booking.Include(x => x.OverlapBookings).ThenInclude(y => y.TargetBooking).FirstOrDefault(x => x.Id == 12);

Should an Entity Framework join contain the table within the parent table?

I've got the following entity defined:
public partial class ImportConnection
{
public int id { get; set; }
public int fkCompanyID { get; set; }
public int fkUserTypeID { get; set; }
public int fkDataConnection { get; set; }
public System.DateTime RecordCreated { get; set; }
public Nullable<System.DateTime> RecordUpdated { get; set; }
public string ConnectionName { get; set; }
public bool isEnabled { get; set; }
public bool isPrimary { get; set; }
public bool ManageUsersOnImport { get; set; }
public bool ManageGroupsOnImport { get; set; }
public bool isValidated { get; set; }
public bool isErrored { get; set; }
public int XLoc { get; set; }
public int YLoc { get; set; }
public int FrequencyInMinutes { get; set; }
public Nullable<System.DateTime> ScheduledStartTime { get; set; }
public Nullable<System.DateTime> ActualStart { get; set; }
public Nullable<System.DateTime> QueuedTime { get; set; }
public Nullable<System.DateTime> ActualCompletion { get; set; }
public int LastRunLengthTime { get; set; }
public string ImportQuery { get; set; }
public string IdentityColumn { get; set; }
public string LastRunMessage { get; set; }
public virtual Company Company { get; set; } //<---- here
public virtual UserType UserType { get; set; }
public virtual DataConnector DataConnector { get; set; } //<---- and here
}
I'm loading the table from the DB using the following query. I'm also joining the two other tables. If I run this in SQL Server Management Studio (having to modify the query a bit) I get the company and dataconnector so I know the data is ok in the DB (the keys match).
List<ImportConnection> dcList = await (from data in db.ImportConnection
join dc in db.DataConnector on data.fkDataConnection equals dc.id
join co in db.Company on data.fkCompanyID equals co.Id
where data.ScheduledStartTime < DateTime.Now
&& data.isEnabled == true
&& dc.isValidated == true
&& !data.isErrored
select data).ToListAsync();
However, after it loads the query I get the data from ImportConnection but Company and DataConnector are both null.
Shouldn't the tables from the join show up in the top entity object? If so, what could be causing the issue with data not showing up?
EF will resort to convention defaults for FKs if you don't configure it with the naming convention your schema uses. When you've set up navigation properties you do not need to add Joins in your Linq expression, just .Include() them to eager-load them, or let EF Lazy Load them on demand.
If you have migrations enabled you may find that your table has columns like "CompanyID" added following EF's default convention.
To configure your FKs:
[ForeignKey("Company")]
public int fkCompanyID { get; set; }
[ForeignKey("UserType")]
public int fkUserTypeID { get; set; }
[ForeignKey("DataConnection")]
public int fkDataConnection { get; set; }
Then to query:
List<ImportConnection> dcList = await db.ImportConnection
.Include(x => x.DataConnector)
.Include(x => x.UserType)
.Include(x => x.Company)
.Where( x => x.ScheduledStartTime < DateTime.Now
&& x.isEnabled
&& x.DataConnector.isValidated
&& !x.isErrored)
.ToListAsync();
The other thing to check is that your relationships are set up properly for a many-to-one, not one to one. In EF6, if a relationship is marked as one-to-one then EF will use the PK on both tables as the relating FK.
I.e. if your EntityTypeConfiguration / modelBuilder config references:
.HasRequired(x => x.Company)
.WithRequired(x => x.ImportConnection)
Where the ImportConnection has a Company and the Company has an ImportConnection, then EF will be joining this CompanyId on Company with ImportConnectionId on ImportConnection. This can lead to issues where expected or incorrect data comes back from relationships.
It should be a many-to-one connection:
.HasRequired(x => x.Company)
.WithMany() // Assuming Company does not have a collection of ImportConnections.
// or .WithMany(x => x.ImportConnections) if it does.
This is one reason I explicitly configure EF rather than rely on convention. This way either EF tells me there is a problem, or behaves the way I can inspect and correct rather than guess and what convention it's interpreting.

Referenced object is not loaded from database

This the table structure I have:
#region Tables
public class WorkoutProfile
{
public WorkoutProfile()
{
WorkoutExercises = new List<WorkoutExercise>();
}
[Key]
public int ProfileId { get; set; }
public string Name { get; set; }
public int Sets { get; set; }
public int RestAfterSetInSeconds { get; set; }
public virtual User User { get; set; }
public virtual ICollection<WorkoutExercise> WorkoutExercises { get; set; }
}
public class WorkoutExercise
{
[Key]
public int WorkoutId { get; set; }
public virtual Exercise Exercise { get; set; }
public int Order { get; set; }
public int WorkoutTimeInSeconds { get; set; }
public int RestAfterInSeconds { get; set; }
}
public class Exercise
{
[Key]
public long ExerciseId { get; set; }
public string Title { get; set; }
public string Visualisation { get; set; }
public bool IsDefault { get; set; } // Is exersice should be included when user first registers
}
public class User
{
[Key]
public long UserId { get; set; }
public string Email { get; set; }
public DateTime Registered { get; set; }
}
#endregion Tables
In the repository class I run the following linq query:
return context
.WorkoutProfiles.Include(w => w.WorkoutExercises)
.Where(q => q.User.UserId == userId && q.ProfileId == profileId)
.FirstOrDefault();
and I receive the good and old "Object reference not set to an instance of an object". When examining the result, see that Exercises property in WorkoutExercises is null.
This is how the database is created using code first approach:
So, the question is: why Exercises not included in WorkoutExercises object? Do I need to include it somehow? I am using .NET Core 2
The simple answer would be no lazy loading in EFCore. Not Released yet but if you want to dabble with alpha code, its in the repository. Based on your classes there are no collections for exercises in WorkoutExcercise.
Then you need to ThenInclude(w => w.Exercises) following your Include clause since EFCore doesn't do lazy loading.
I found a solution following this post
Altered my code as following:
var top = context
.Set<WorkoutProfile>()
.Where(q => q.ProfileId == profileId && q.User.UserId == userId)
.Include(q => q.WorkoutExercises)
.SingleOrDefault();
context
.Entry(top)
.Collection(e => e.WorkoutExercises)
.Query()
.OfType<WorkoutExercise>()
.Include(e => e.Exercise)
.Load();
And it worked

Entityframework core how to map Many-to-Many without creating a join table

I am trying this activity to clone from existing .edmx project to code first.
I have two entities. I want to have many to many relation without creating a new table. I am using EF Core 2.0 code first approach.
Please find below the entity i have created. I am not sure whether is this is the right way to do this.
I would like to have the foreign key column on both the tables ie. WorkflowId and WorkCaseId on WorkCase and Workflow tables respectively.
public class WorkCase
{
[Key]
public int WorkCaseId { get; set; }
public int WorkflowId { get; set; }
public int CaseDetailId {get;set;}
public CaseDetail CaseDetail {get;set;}
public WorkFlow WorkFlow { get; set; }
public ICollection<WorkFlow> WorkFlows { get; set; }
}
public class WorkFlow : BaseEntity
{
[Key]
public int WorkFlowId { get; set; }
public string Comment { get; set; }
public DateTime? UpdateDate { get; set; }
public int WorkCaseId { get; set; }
public WorkCase WorkCase { get; set; }
public ICollection<WorkCase> WorkCases { get; set; }
}
My expectation is as below. Can anyone help how to achieve the EF Core configuration:
- Workcase will have the latest workflowid
- workflow will have history for each workcaseid.
Thanks
UPDATE: EF5+ supports many-to-many without explicitly mapping the join table. See this EF announcement
As commented on your question, EF Core <= 2.2 does not yet support many-to-many relationships without a join table. This is an issue tracked in the EF Core repo's backlock, and will maybe make it into version 3.0.
In your case you'll need to introduce a new table that relates to both parent tables. In this case, your model will like something like the following:
public class WorkCase
{
public int WorkCaseId { get; set; }
public int CaseDetailId { get; set; }
public CaseDetail CaseDetail { get; set; }
public ICollection<WorkCaseWorkflow> Workflows { get; set; }
}
public class Workflow
{
public int WorkflowId { get; set; }
public string Comment { get; set; }
public DateTime? UpdateDate { get; set; }
public ICollection<WorkCaseWorkflow> WorkCases { get; set; }
}
public class WorkCaseWorkflow
{
public int WorkCaseId { get; set; }
public WorkCase WorkCase { get; set; }
public int WorkflowId { get; set; }
public Workflow Workflow { get; set; }
}
Then in your DbContext subclass, override the OnModelCreating and add the following code:
protected override void OnModelCreating(ModelBuilder builder)
{
var ww = builder.Entity<WorkCaseWorkflow>();
ww.HasKey(w => new { w.WorkCaseId, WorkflowId });
ww.HasOne(w => w.WorkCase)
.WithMany(wc => wc.Workflows)
.HasForeignKey(w => w.WorkCaseId);
ww.HasOne(w => w.Workflow)
.WithMany(wc => wc.WorkCases)
.HasForeignKey(w => w.WorkflowId);
}
I'm not familiar with your model, but you can move the shared properties to the join table.
However, there is a great series of articles by #Arthur Vickers (a member in the EF Core dev team) on how to easen up many-to-many relationships:
Part 1: The basics
Part 2: Hiding as IEnumerable
Part 3: Hiding as ICollection
Part 4: A more general abstraction

EF Core Include() doesn't query all childs

I have this model:
public class RepairRequest
{
[Key]
public int Id { get; set; }
public List<RepairAction> RepairActions { get; set; }
public decimal TotalPrice => RepairActions.Sum(r => r.ActionPrice);
public string LastOperation => RepairActions.LastOrDefault().RepairOperation.Description;
}
public class RepairAction
{
[Key]
public int Id { get; set; }
public int RepairRequestId { get; set; }
public RepairRequest RepairRequest { get; set; }
public int RepairOperationId { get; set; }
public RepairOperation RepairOperation { get; set; }
public decimal ActionPrice { get; set; }
}
public class RepairOperation
{
[Key]
public int Id { get; set; }
public string Description { get; set; }
}
I'm trying to query RepairRequests and get TotalPrice and also LastOperation in a List but doesn't work for both properties. This is what I have tried till now:
using (var context = new ServiceManagerContext(new DbContextOptions<ServiceManagerContext>())) {
var data = context.RepairRequests
.Include(r => r.RepairActions).ThenInclude(r => r.RepairOperation); // Only LastAction works
//.Include("RepairActions").Include("RepairActions.RepairOperation"); // Only LastAction works
//.Include(r => r.RepairActions); // Only TotalPrice works
//.Include("RepairActions"); // Only TotalPrice works
var repairRequest = data.FirstOrDefault(r => r.Id == 5);
Assert.NotNull(repairRequest);
Assert.Equal(60.0m, repairRequest.RepairPrice);
Assert.Equal("Παραδόθηκε", repairRequest.LastAction);
}
Thank you.
I'd consider avoiding attempting to resolve calculated properties in your domain entities and instead look to resolve those when querying the data to populate view models.
If your view model needs the TotalPrice and LastOperation, then provided a Repository or such returning IQueryable you can expand the query to return what is needed using deferred execution rather than attempting to rely on eager loading the entire tree:
I.e.
IQueryable<RepairRequest> requests = context.RepairRequests.Where(x => x.Id == 5); // Or pull from a Repository returning the IQueryable
var viewModelData = requests.Select(x => new {x.Id, TotalPrice = x.RepairActions.Sum(), LastOperation = x.RepairActions.LastOrDefault()?.RepairOperation?.Description }).SingleOrDefault();
This should execute a more optimized query and return you an anonymous type with just the data you need to populate whatever view model you want to display. The iffy bit is around situations where there are no repair actions, or a repair action without an operation.. EF should avoid the null ref and just return null. the ?. syntax may not be necessary or supported, so it may just need to be ".". Using a method where you eager or lazy load those related entities and execute Linq off the entity instances, be careful around .SingleOrDefault() and drilling down into child fields.
Firstaball you have to declare Foreign Keys, and flag virtual properties like :
public class RepairRequest
{
[Key]
public int Id { get; set; }
public virtual ICollection<RepairAction> RepairActions { get; set; }
public decimal TotalPrice => RepairActions.Sum(r => r.ActionPrice);
public string LastOperation => RepairActions.LastOrDefault().RepairOperation.Description;
}
public class RepairAction
{
[Key]
public int Id { get; set; }
public decimal ActionPrice { get; set; }
public int RepairRequestId { get; set; }
[ForeignKey("RepairRequestId ")]
public virtual RepairRequest RepairRequest { get; set; }
public int RepairOperationId { get; set; }
[ForeignKey("RepairOperationId")]
public RepairOperation RepairOperation { get; set; }
}
Then you could call this, which load all children values :
var data = context.RepairRequests.Include("RepairActions.RepairOperation");