I'm experiencing odd behaviour on a quite simple task. I'm using EF Core 2.1 with SQL Server 2016, I have the following code that basically creates three objects and inserts them into the database using DbContext.Add().
And all three Add() operations return successfully, with properly created entities, however, the JobSchedule is never inserted into the database when calling SaveChangesAsync(), and I have no clue.
private async Task CreateXPTOJob(XPTOJobModel model)
{
var jobData = new XPTOJobData
{
Id = Guid.NewGuid(),
Foo = model.Foo,
Bar= model.Bar
};
Context.XPTOJobData.Add(jobData);
var jobType = await Context.JobTypes.FindByCode(EJobType.XPTO);
var jobPriority = await Context.JobPriorities.FindByCode(EJobPriority.Normal);
var jobStatus = await Context.JobStatuses.FindByCode(EJobStatus.Created);
var job = new Job
{
Id = Guid.NewGuid(),
OwnerId = UserId,
PriorityId = jobPriority.Id,
TypeId = jobType.Id,
StatusId = jobStatus.Id,
MaxRetries = 3,
XPTOJobDataId = jobData.Id
};
Context.Jobs.Add(job);
var scheduleFrequency = await Context.ScheduleFrequencies.FindByCode(EScheduleFrequency.Once);
var schedule = new JobSchedule
{
Id = Guid.NewGuid(),
Enabled = true,
FrequencyId = scheduleFrequency.Id,
JobId = jobId,
NotifyCompletion = true,
PreferredStartTime = DateTime.Now
};
Context.JobSchedules.Add(schedule);
await Context.SaveChangesAsync();
}
If a look at the debug output, I can see the four Selects, and two inserts, as bellow:
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (3ms) [Parameters=[@__type_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [jobType].[Id], [jobType].[Code], [jobType].[Description], [jobType].[DisplayName], [jobType].[Name]
FROM [JobQueue].[JobTypes] AS [jobType]
WHERE [jobType].[Code] = @__type_0
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (2ms) [Parameters=[@__priority_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [jobPriority].[Id], [jobPriority].[Code], [jobPriority].[Description], [jobPriority].[DisplayName], [jobPriority].[Name]
FROM [JobQueue].[JobPriorities] AS [jobPriority]
WHERE [jobPriority].[Code] = @__priority_0
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (1ms) [Parameters=[@__status_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [jobStatus].[Id], [jobStatus].[Code], [jobStatus].[Description], [jobStatus].[DisplayName], [jobStatus].[Name]
FROM [JobQueue].[JobStatuses] AS [jobStatus]
WHERE [jobStatus].[Code] = @__status_0
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (1ms) [Parameters=[@__frequency_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [scheduleFrequency].[Id], [scheduleFrequency].[Code], [scheduleFrequency].[Description], [scheduleFrequency].[DisplayName], [scheduleFrequency].[Name]
FROM [JobQueue].[ScheduleFrequencies] AS [scheduleFrequency]
WHERE [scheduleFrequency].[Code] = @__frequency_0
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (1ms) [Parameters=[@p0='?' (DbType = Guid), @p1='?' (DbType = Guid), @p2='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [LifeCycle].[XPTOJobData] ([Id], [Foo], [Bar])
VALUES (@p0, @p1, @p2);
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (1ms) [Parameters=[@p3='?' (DbType = Guid), @p4='?' (DbType = Guid), @p5='?' (DbType = Int32), @p6='?' (DbType = Guid), @p7='?' (DbType = Guid), @p8='?' (DbType = Guid), @p9='?' (DbType = Guid), @p10='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [JobQueue].[Jobs] ([Id], [YPTOJobDataId], [MaxRetries], [XPTOJobDataId], [OwnerId], [PriorityId], [StatusId], [TypeId])
VALUES (@p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10);
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action method JobQueue.Controllers.JobsController.Post (JobQueue), returned result Microsoft.AspNetCore.Mvc.ObjectResult in 63.4302ms.
All the FindByCode extensions follow the same logic:
public static Task<ScheduleFrequency> FindByCode(this IQueryable<ScheduleFrequency> queryable, EScheduleFrequency frequency)
{
return queryable.AsNoTracking().SingleAsync(scheduleFrequency => scheduleFrequency.Code == frequency);
}
Any ideas why the third insert is not being executed? I've tried a lot of small changes and tweaks, but unsuccessfully. Anyways, thank you for your time and help!
Edit 1: I'm putting more related code bellow.
DbContext
public class MyDbContext : DbContext
{
...
public DbSet<User> Users { get; set; }
public DbSet<XPTOJobData> XPTOJobData { get; set; }
public DbSet<Job> Jobs { get; set; }
public DbSet<JobPriority> JobPriorities { get; set; }
public DbSet<JobSchedule> JobSchedules { get; set; }
public DbSet<JobStatus> JobStatuses { get; set; }
public DbSet<JobType> JobTypes { get; set; }
public DbSet<ScheduleFrequency> ScheduleFrequencies { get; set; }
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new XPTOJobDataConfiguration());
modelBuilder.ApplyConfiguration(new JobConfiguration());
modelBuilder.ApplyConfiguration(new JobPriorityConfiguration());
modelBuilder.ApplyConfiguration(new JobScheduleConfiguration());
modelBuilder.ApplyConfiguration(new JobStatusConfiguration());
modelBuilder.ApplyConfiguration(new JobTypeConfiguration());
modelBuilder.ApplyConfiguration(new ScheduleFrequencyConfiguration());
}
}
Job
public class Job
{
// Properties
public Guid Id { get; set; }
public Guid? XPTOJobDataId { get; set; }
public Guid OwnerId { get; set; }
public Guid PriorityId { get; set; }
public Guid StatusId { get; set; }
public Guid TypeId { get; set; }
public ushort MaxRetries { get; set; }
// Navigation Properties
public XPTOJobData XPTOJobData { get; set; }
public User Owner { get; set; }
public JobPriority Priority { get; set; }
public JobStatus Status { get; set; }
public JobType Type { get; set; }
// Navigation Related Properties
public ICollection<JobSchedule> JobSchedules => _jobSchedules?.ToList();
private HashSet<JobSchedule> _jobSchedules;
public Job()
{
_jobSchedules = new HashSet<JobSchedule>();
}
}
JobPriority
public enum EJobPriority
{
Normal,
High,
Immediate
}
public class JobPriority
{
// Properties
public Guid Id { get; set; }
public EJobPriority Code { get; set; }
public string Description { get; set; }
public string DisplayName { get; set; }
public string Name { get; set; }
// Navigation Related Properties
public ICollection<Job> Jobs => _jobs?.ToList();
private HashSet<Job> _jobs;
public JobPriority()
{
_jobs = new HashSet<Job>();
}
}
JobSchedule
public class JobSchedule
{
// Properties
public Guid Id { get; set; }
public bool Enabled { get; set; }
public DateTime? EffectiveDate { get; set; }
public DateTime? ExpiryDate { get; set; }
public Guid FrequencyId { get; set; }
public Guid JobId { get; set; }
public string Name { get; set; }
public DateTime? NextRunDate { get; set; }
public bool NotifyCompletion { get; set; }
public DateTime PreferredStartTime { get; set; }
public string Recurrence { get; set; }
// Navigation Properties
public Job Job { get; set; }
public ScheduleFrequency Frequency { get; set; }
}
JobConfiguration
public class JobConfiguration : AEntityTypeConfiguration<Job>
{
protected override string TableName => "Jobs";
protected override string SchemaName => Schemas.JobQueue;
protected override void ConfigureForeignKeys(EntityTypeBuilder<Job> entity)
{
entity.HasOne(job => job.XPTOJobData)
.WithMany()
.HasConstraintName(CreateForeignKeyName("XPTOJobDataId"))
.OnDelete(DeleteBehavior.SetNull);
entity.HasOne(job => job.Owner)
.WithMany(user => user.Jobs)
.HasConstraintName(CreateForeignKeyName("OwnerId"))
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(job => job.Priority)
.WithMany(jobPriority => jobPriority.Jobs)
.HasConstraintName(CreateForeignKeyName("PriorityId"))
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(job => job.Status)
.WithMany(jobStatus => jobStatus.Jobs)
.HasConstraintName(CreateForeignKeyName("StatusId"))
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(job => job.Type)
.WithMany(jobType => jobType.Jobs)
.HasConstraintName(CreateForeignKeyName("TypeId"))
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
}
}
JobPriorityConfiguration
public class JobPriorityConfiguration : AEntityTypeConfiguration<JobPriority>
{
protected override string TableName => "JobPriorities";
protected override string SchemaName => Schemas.JobQueue;
protected override void ConfigureProperties(EntityTypeBuilder<JobPriority> entity)
{
entity.Property(jobPriority => jobPriority.Code)
.IsRequired();
entity.Property(jobPriority => jobPriority.Description)
.HasMaxLength(255)
.IsRequired();
entity.Property(jobPriority => jobPriority.DisplayName)
.HasMaxLength(50)
.IsRequired();
entity.Property(jobPriority => jobPriority.Name)
.HasMaxLength(50)
.IsRequired();
}
protected override void ConfigureIndexes(EntityTypeBuilder<JobPriority> entity)
{
entity.HasIndex(x => x.Code)
.IsUnique()
.HasName(CreateUniqueKeyName("Code"));
entity.HasIndex(x => x.Name)
.IsUnique()
.HasName(CreateUniqueKeyName("Name"));
}
}
JobScheduleConfiguration
public class JobScheduleConfiguration : AEntityTypeConfiguration<JobSchedule>
{
protected override string TableName => "JobSchedules";
protected override string SchemaName => Schemas.JobQueue;
protected override void ConfigureProperties(EntityTypeBuilder<JobSchedule> entity)
{
entity.Property(jobSchedule => jobSchedule.Name)
.HasMaxLength(255)
.IsRequired();
entity.Property(jobSchedule => jobSchedule.Recurrence)
.HasMaxLength(50);
}
protected override void ConfigureIndexes(EntityTypeBuilder<JobSchedule> entity)
{
entity.HasIndex(jobSchedule => jobSchedule.Name)
.HasName(CreateIndexName("Name"));
}
protected override void ConfigureForeignKeys(EntityTypeBuilder<JobSchedule> entity)
{
entity.HasOne(jobSchedule => jobSchedule.Job)
.WithMany(job => job.JobSchedules)
.HasConstraintName(CreateForeignKeyName("JobId"))
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
}
}
I believe your side effect is being caused by the properties following this signature.
public ICollection<JobSchedule> JobSchedules => _jobSchedules?.ToList();
DbSet properties of virtual ICollection<>
no longer means the same thing. This does not enable lazy loading of the navigation property. You must enable it in the DbContext configuration.
Additional takeaways
According to the content of the post, you are making a proof of concept that EF 6 can be migrated to EF Core. I think the cause of your problems are side effect behaviors. EF Core focuses on a convention first approach, whereas EF 6 requires verbose configuration. Let those conventions do the work for you.
For instance, your classes (the ones that you have shared) deriving from AEntityTypeConfiguration<>
are almost completely restating the default conventions, with the exception of the explicit naming of the Foreign Key Restraint. .HasConstraintName(CreateForeignKeyName("XPTOJobDataId"))
If you have the ability to switch to EF Core's Fkey naming scheme, then that's a lot of code that does not need to be written. At least 3 classes and an interface would not need to be written.