.Net Core 3.1 — Невозможно отследить экземпляр объекта типа «Город» — при использовании пользовательского отслеживания дат

#c# #entity-framework #asp.net-core #entity-framework-core

#c# #entity-framework #asp.net-core #entity-framework-core

Вопрос:

Я знаю, что подобный вопрос задавался много раз, но, к сожалению, после нескольких дней исследований до сих пор не удалось найти решение моей проблемы. Я пытаюсь подробно объяснить, в чем проблема. Ниже приведен простой проект, который может воспроизвести проблему!

Проблема:

Я использую пользовательский трекер дат, чтобы я мог не только добавлять созданную дату или обновленную дату во время сохранения объектов, но и мягко удалять свои записи.

Однако, когда я использую это, я получаю следующую ошибку

 System.InvalidOperationException: 'The instance of entity type 'City' cannot be tracked because another instance with the key value '{Id: a6606535-76a5-4a23-b204-08d882481a95}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.'
  

Странно то, что это случается только иногда, я бы сказал, когда я запускаю проект в первый раз, это происходит в 90% случаев, но затем, если я обойду исключение и попаду в конечную точку, это произойдет только в 10% случаев.

Код:

Модели:

     public class CitySync : ICreatedDateTracking
    {
        public long Id { get; set; }
        public Guid CityId { get; set; }
        public virtual City City { get; set; }
        public DateTimeOffset CreatedDate { get; set; }
        public int Added { get; set; }
    }

    public partial class School : IDateTracking
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public DateTimeOffset CreatedDate { get; set; }
        public virtual City City { get; set; }
        public Guid CityId { get; set; }
        public DateTimeOffset? DeletedDate { get; set; }
        public DateTimeOffset UpdatedDate { get; set; }
    }

    public partial class City : IDateTracking
    {

        public Guid Id { get; set; }
        public DateTimeOffset CreatedDate { get; set; }
        public string OwnerName { get; set; }
        public virtual List<School> Schools { get; set; }
        public virtual ICollection<CitySync> CitySync { get; set; }
        public DateTimeOffset? DeletedDate { get; set; }
        public DateTimeOffset UpdatedDate { get; set; }
    }
  

DbContext:

 
 public class ApplicationDbContext : DbContext
    {


        private readonly IChangeTracker _changeTracker;

        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IChangeTracker changeTracker)
            : base(options)
        {
            _changeTracker = changeTracker;
        }
        public void DetachAllEntities()
        {
            var changedEntriesCopy = this.ChangeTracker.Entries()
                .Where(e => e.State == EntityState.Added ||
                            e.State == EntityState.Modified ||
                            e.State == EntityState.Deleted)
                .ToList();

            foreach (var entry in changedEntriesCopy)
                entry.State = EntityState.Detached;
        }

        public override int SaveChanges(bool acceptAllChangesOnSuccess)
        {
            Task.Run(async () => await _changeTracker.BeforeSaveChanges(this));
            var saveChanges = base.SaveChanges(acceptAllChangesOnSuccess);
            Task.Run(async () => await _changeTracker.AfterSaveChanges(this));
            return saveChanges;
        }

        public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken())
        {
            await _changeTracker.BeforeSaveChanges(this);
            var saveChangesAsync = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
            await _changeTracker.AfterSaveChanges(this);
            return saveChangesAsync;
        }


        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {

            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<School>(entity =>
            {
                entity.Property(e => e.Id).HasMaxLength(50);
                entity.Property(e => e.Name).HasMaxLength(255);
                entity.HasOne(x => x.City).WithMany(x => x.Schools);
                entity.HasKey(x => new
                {
                    x.Id,
                    x.CitytId
                });

            });

            modelBuilder.Entity<City>(entity =>
            {
                entity.Property(e => e.OwnerName).HasMaxLength(255);
                entity.HasMany(x => x.Schools).WithOne(x => x.City);
            });

            modelBuilder.Entity<CitySync>(entity =>
            {
                entity.HasOne(x => x.City).WithMany(x => x.CitySync);
            });
            foreach (var entityType in modelBuilder.Model.GetEntityTypes().Where(e => typeof(IDateTracking).IsAssignableFrom(e.ClrType)))
            {
                if (entityType.BaseType == null)
                {
                    modelBuilder.Entity(entityType.ClrType).HasQueryFilter(ConvertFilterExpression<IDateTracking>(e => e.DeletedDate == null, entityType.ClrType));
                }
            }
        }

        private static LambdaExpression ConvertFilterExpression<TInterface>(Expression<Func<TInterface, bool>> filterExpression, Type entityType)
        {
            var newParam = Expression.Parameter(entityType);
            var newBody = ReplacingExpressionVisitor.Replace(filterExpression.Parameters.Single(), newParam, filterExpression.Body);
            return Expression.Lambda(newBody, newParam);
        }

        public virtual DbSet<School> Schools { get; set; }
        public virtual DbSet<City> Cities { get; set; }
        public virtual DbSet<CitySync> CitySync { get; set; }

    }

  

DateChangeTracker:

 
 public class DateChangeTracker : IChangeTracker
    {
        public static EntityState[] AuditedEntityStates = { EntityState.Added, EntityState.Modified, EntityState.Deleted };

        public DateChangeTracker()
        {
        }

        public virtual Task BeforeSaveChanges(DbContext dbContext)
        {
            var now = DateTimeOffset.UtcNow;
            foreach (var entry in dbContext.ChangeTracker.Entries().Where(e => AuditedEntityStates.Contains(e.State)).ToList())
            {
                if (entry.Entity is ICreatedDateTracking createdDateTracking)
                {
                    if (entry.State == EntityState.Added)
                    {
                        createdDateTracking.CreatedDate = now;
                    }
                }
                if (entry.Entity is IUpdatedCreatedDateTracking updatedCreatedDateTracking)
                {
                    updatedCreatedDateTracking.UpdatedDate = now;
                }
                if (entry.Entity is IDateTracking dateTracking)
                {
                    if (entry.State == EntityState.Added)
                    {
                        dateTracking.CreatedDate = now;
                    }
                    if (entry.State == EntityState.Deleted)
                    {

                        entry.State = EntityState.Modified;
                        dateTracking.DeletedDate = now;
                    }
                }
            }
            return Task.CompletedTask;
        }

        public virtual Task AfterSaveChanges(DbContext dbContext)
        {
            return Task.CompletedTask;
        }

    }


  

DbContext Factory:

 
    public class ApplicationDbContextDesignTimeDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
    {
        public ApplicationDbContext CreateDbContext(string[] args)
        {
            var configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .Build();
            var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
            var connectionString = configuration.GetConnectionString("DefaultConnection");
            builder.UseSqlServer(connectionString);
            return new ApplicationDbContext(builder.Options, new DateChangeTracker());
        }
    }
  
  • IDate interfaces, implement created, updated, deleted fileds

SchoolService:

 
  public class SchoolService : ISchoolService
    {
        private readonly ApplicationDbContext applicationDbContext;


        public SchoolService(ApplicationDbContext applicationDbContext)
        {
            this.applicationDbContext = applicationDbContext;
        }

        public Guid PopulateCity()
        {
            var existingCity = applicationDbContext.Cities.FirstOrDefault();
            if (existingCity == null)
            {
                var city = new Models.City { CreatedDate = DateTimeOffset.Now, OwnerName = "test" };
                applicationDbContext.Cities.Add(city);
                applicationDbContext.SaveChanges();
                return city.Id;
            }
            return existingCity.Id;
            
        }


        public void ProcessSchools(Guid cityId)
        {
            var city = applicationDbContext.Cities.FirstOrDefault(x => x.Id == cityId);
            if (city == null)
                throw new Exception("City doesnt exist");

            var citySync = new CitySync
            {
                CityId = city.Id,
                CreatedDate = DateTimeOffset.Now,
            };
            var existingSchools = applicationDbContext.Schools.Where(x => x.CitytId == cityId).ToList();
            // update schools if the exists 
            // add new ones if they dont
            var schools = new List<School>();

            if (!existingSchools.Any())
            {
                schools.Add(new School { CitytId = cityId, CreatedDate = DateTimeOffset.Now, Id = "1", Name = "school1" });
                schools.Add(new School { CitytId = cityId, CreatedDate = DateTimeOffset.Now, Id = "2", Name = "school2" });
                schools.Add(new School { CitytId = cityId, CreatedDate = DateTimeOffset.Now, Id = "3", Name = "school3" });
            }
            else
            {

                foreach (var item in existingSchools)
                {
                    item.UpdatedDate = DateTimeOffset.Now;
                }
                applicationDbContext.SaveChanges();
            }

            var additions = schools.Except(existingSchools, new SchoolComparer()).ToList();
            foreach (var school in additions)
            {
                school.CitytId = city.Id;
            }
            applicationDbContext.Schools.AddRange(additions);
            city.UpdatedDate = DateTimeOffset.Now;
            city.OwnerName = "Updated Name";
            citySync.Added = additions.Count;
            applicationDbContext.CitySync.Add(citySync);
            applicationDbContext.SaveChanges();
        }
    }

    public class SchoolComparer : IEqualityComparer<School>
    {
        public bool Equals(School x, School y)
        {
            return x?.Id == y?.Id;
        }

        public int GetHashCode(School obj)
        {
            return 0;
        }
    }


  

Ошибка возникает прямо в следующей строке

             *applicationDbContext.CitySync.Add(citySync);*
  

Запуск:

 
    public class Startup
    {
        public Startup(IConfiguration configuration, IHostEnvironment environment)
        {
            Configuration = configuration;
            Environment = environment;
        }
        public IHostEnvironment Environment { get; }
        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped(typeof(IChangeTracker), typeof(DateChangeTracker));

            services.AddTransient<ISchoolService, SchoolService>();
            var connectionString = Configuration.GetConnectionString("DefaultConnection");
            services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer(connectionString); options.EnableSensitiveDataLogging(); }, ServiceLifetime.Scoped);
            services.AddControllers();
            services.AddHttpContextAccessor();

        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
            {
                serviceScope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
            }

        }
    }

  

Пожалуйста, обратите внимание, что это происходит только в том случае, если я использую отслеживание дат, если я не использую переопределенные методы SaveChanges, тогда все работает нормально.

Комментарии:

1. Я бы предположил public Guid CitytId { get; set; } (обратите внимание на написание CitytId), из-за чего у EF возникают проблемы с определением того, к какому другому объекту это относится.

2. Извините, это была моя опечатка при публикации вопроса, в самом приложении нет опечатки

Ответ №1:

Пожалуйста, попробуйте использовать этот способ

 services.AddDbContext<ApplicationDbContext>(options =>options.UseSqlServer(connectionString,x => x.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)),ServiceLifetime.Transient);

services.AddTransient<IApplicationDbContext>(provider => provider.GetService<ApplicationDbContext>());
  

Комментарии:

1. Спасибо @Goti, как это поможет? Я попробовал, но это приводит к сбою приложения, и оно просто выскакивает!