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);
}
}
AsNoTrackingthere. Shouldn't make a difference, but with ef-core you never can tell...The association between entity types 'JobPriority' and 'Job' has been severed but the relationship is either marked as 'Required' or is implicitly required because the foreign key is not nullable. If the dependent/child entity should be deleted when a required relationship is severed, then setup the relationship to use cascade deletes. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values.ScheduleFrequencyto the change tracker.PriorityIdetc.), that is, without querying the entire entities. If that works at least you have a work-around. Well, it's better anyway because the queries become much leaner that way.