EF Core - DbContext.SaveChangesAsync() not persisting data in database

c# entity-framework-core

Question

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);
    }
}
1
2
8/7/2018 12:11:18 AM

Accepted Answer

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.

1
6/17/2018 1:30:55 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow