EF 6 not detecting existing subclass when creating a new parent class

Viewed 54

I have a Topic class:

 public class Topic : BaseEntity
    {
        [MaxLength(50)]
        public string Name { get; set; }
        public bool Active { get; set; }
        public ICollection<Content> Contents { get; set; }
    }

And a Content class:

public class Content : BaseEntity
    {
        [Required]
        [RegularExpression(@"^[\d \w \s]+$", ErrorMessage = "The field {0} Characters are not allowed.")]
        public string Name { get; set; }
        [Required]
        [Url]
        public string URL { get; set; }
        [Required]
        [Range(0.1, double.MaxValue, ErrorMessage = "The field {0} must be greater than {1}.")]
        public double StartingVersion { get; set; }
        [Required]
        [Range(0.1, double.MaxValue, ErrorMessage = "The field {0} must be greater than {1}.")]
        public double EndingVersion { get; set; }
        [Required]
        [MinLength(1)]
        public string Summary { get; set; }
        [Required]
        public bool Active { get; set; }
        [Required]
        public ICollection<Topic> Topics { get; set; }
}

The BaseEntity looks like this:

   public class BaseEntity
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        public int CreatedBy { get; set; }
        public DateTime CreatedDate { get; set; }
        public int ModifiedBy { get; set; }
        public DateTime ModifiedDate { get; set; }
    }

My DataContext looks like this:

   public DataContext(DbContextOptions<DataContext> options) : base(options) { }

        private DbSet<Topic> Topics { get; set; }
        private DbSet<Content> Contents { get; set; }
       
        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<Content>()
                .HasIndex(u => u.Id)
                .IsUnique();

            builder.Entity<Content>().HasMany<Topic>(t => t.Topics).WithMany(c => c.Contents);

            base.OnModelCreating(builder);
        }

And I'm trying to use a generic Repository. The saveEntity looks like this:

 public async Task<T> SetEntity<T>(T entity) where T : BaseEntity
        {
            using (var scope = _serviceProvider.CreateScope())
            {
                entity.CreatedDate = DateTime.Now;
                var _dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();
                _dbContext.Add(entity);
                await _dbContext.SaveChangesAsync("me@me.com");
                return entity;
            }
        }

And the Content Service method that does the creation of Contents looks like this:

   public async Task<ContentDTO> AddContentAsync(ContentDTO content)
        {
            Expression<Func<Content, bool>> exp = i => i.URL == content.URL && i.Active == true;
            if (_dataRepository.GetEntities(exp).Any())
            {
                throw new DuplicateWaitObjectException("Object already exist");
            }

            CheckObjectives(content.Objectives);
            foreach (var item in content.Topics)
            {
                Expression<Func<Topic, bool>> expTopic = i => i.Id == item.Id && i.Active == true;
                var topic = await _dataRepository.GetEntity(expTopic);
                if(topic == null)
                {
                    throw new KeyNotFoundException($"Topic with ID {item.Id} not found");
                }
            }

            Content toSaveContent = Mapping.Mapper.Map<Content>(content);
            _modelHelper.ModelValidation(toSaveContent);
            toSaveContent.Active = true;

            Content newContent = await _dataRepository.SetEntity(toSaveContent);

            return Mapping.Mapper.Map<ContentDTO>(newContent);
        }

My problem is that when I try to create a new Content EF fails to detect that the Topics included in the body of the Content are existing ones and tries to add them as new in the DB. Obviously, this raises a SQL exception saying I can't define the Id of the Topic.

What I'm missing??

Thank you for your help

EDIT: Also tried to retrieve the Topics from context, but didn't work either:

public async Task<ContentDTO> AddContentAsync(ContentDTO content)
        {
            Expression<Func<Content, bool>> exp = i => i.URL == content.URL && i.Active == true;
            if (_dataRepository.GetEntities(exp).Any())
            {
                throw new DuplicateWaitObjectException("Object already exist");
            }

            CheckObjectives(content.Objectives);

            Content toSaveContent = Mapping.Mapper.Map<Content>(content);
            _modelHelper.ModelValidation(toSaveContent);
            toSaveContent.Active = true;

            toSaveContent.Topics = new List<Topic>();
            foreach (var item in content.Topics)
            {
                Expression<Func<Topic, bool>> expTopic = i => i.Id == item.Id && i.Active == true;
                var topic = await _dataRepository.GetEntity(expTopic);
                if(topic == null)
                {
                    throw new KeyNotFoundException($"Topic with ID {item.Id} not found");
                }
                toSaveContent.Topics.Add(topic);
            }

            Content newContent = await _dataRepository.SetEntity(toSaveContent);

            return Mapping.Mapper.Map<ContentDTO>(newContent);
        }

EDIT2: You are right, Guru Stron, I'll extract the GetEntity from the foreach and just take them all before. This is my GetEntity method in the generic repository:

public async Task<T> GetEntity<T>(Expression<Func<T, bool>> predicate) where T : BaseEntity
        {
            using (var scope = _serviceProvider.CreateScope())
            {
                var _dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();
                return _dbContext.Set<T>().Where(predicate).FirstOrDefault();
            }
        }
2 Answers

EF Core uses concept of change tracking to manage data changes.

You should not create scope inside you generic repository (assuming you have default scoped context registration) - each scope will have it's own database context with it's own tracking, so the context which performs saving will have no idea about related entities and consider them as new ones (as you observe).

Usual approach is to have the outside control to control over scope, for example in ASP.NET Core the framework will create a scope on per request level and usually the dbcontext is shared on per request/scope basis.

So you need to remove the manual scope handling in the repository and use constructor injection so the repository shares the change tracking information between get and save queries, otherwise you will need to write some cumbersome code which will find and attach existing related entities in all the navigation properties of the saved entity.

You are creating the PK twice.

in base entity you have

    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

Then in the DBContext. Oncreating you have

 builder.Entity<Content>()
            .HasIndex(u => u.Id)
            .IsUnique();

That aside, when you are working with Entity Framework, do note that it works on instances. I recommend you define the define instances using dependency injection. Please note that the most important thing is to know when and if the DBEntity has disposed the instance it is working with.

Related