Updating and Deleting Associated Records in a One-To-Many Relationship using Entity Framework - entity-framework

Using EF 4.1 Code First, I have a Member entity and it in turn has two "one-to-many" relationships for a HomeAddress and WorkAddress. It also has a boolean property to state whether or not to use either of these addresses.
I have two issues that I can't figure out:
Whenever I update a member's address, a new record is added to the MemberAddresses table (with a new ID value) and the existing record is not deleted. Though it looks fine from the front-end perspective as the HomeAddressId and WorkAddressId in the parent Members table is updated with the new record, the old records are kept in the table (orhpaned). I don't want it to add a new address record when the address is updated. I only want it to update the existing record. If it has to add a new one, then I at least want it to clear out the old one.
There are times that I want to delete the address record from the table. For example, if the member previously had an associated HomeAddress and later the DontUseHomeAddress is set to true, I want the address to be deleted from the table. So far, I have tried setting it to null, but that just prevents any updates. It doesn't delete it.
I'm sure there just some code piece I'm missing, so I'm including all the relevant code below that I think might affect this.
public abstract class Entity
{
public int Id { get; set; }
}
public class Member : Entity
{
public string Name { get; set; }
public bool DontUseHomeAddress { get; set; }
public virtual MemberAddress HomeAddress { get; set; }
public bool DontUseWorkAddress { get; set; }
public virtual MemberAddress WorkAddress { get; set; }
//... other properties here ...
}
public class MemberMap : EntityTypeConfiguration<Member>
{
public MemberMap()
{
ToTable("Members");
Property(m => m.Name).IsRequired().HasMaxLength(50);
//TODO: Somehow this is creating new records in the MemberAddress table instead of updating existing ones
HasOptional(m => m.HomeAddress).WithMany().Map(a => a.MapKey("HomeAddressId"));
HasOptional(m => m.WorkAddress).WithMany().Map(a => a.MapKey("WorkAddressId"));
}
}
public class MemberAddressMap : EntityTypeConfiguration<MemberAddress>
{
public MemberAddressMap()
{
ToTable("MemberAddresses");
Property(x => x.StreetAddress).IsRequired().HasMaxLength(255);
Property(x => x.City).IsRequired().HasMaxLength(50);
Property(x => x.State).IsRequired().HasMaxLength(2);
Property(x => x.ZipCode).IsRequired().HasMaxLength(5);
}
}
Here is the InsertOrUpdate method from my repository class that my controller calls:
public class Repository<TEntity> : IRepository<TEntity> where TEntity : Entity
{
private readonly EfDbContext _context;
private readonly DbSet<TEntity> _dbSet;
public Repository(EfDbContext context)
{
_context = context;
_dbSet = _context.Set<TEntity>();
}
public bool InsertOrUpdate(TEntity entity)
{
if(entity.Id == 0)
{
_dbSet.Add(entity);
}
else
{
_context.Entry(entity).State = EntityState.Modified;
}
_context.SaveChanges();
return true;
}
//... Other repository methods here ...
}
EDIT: Adding in code for UnitOfWork and MemberServices
public class MemberServices : IMemberServices
{
private readonly IUnitOfWork _unitOfWork;
private readonly IRepository _memberRepository;
public MemberServices(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
_memberRepository = unitOfWork.RepositoryFor<Member>();
}
public Member Find(int id)
{
return _memberRepository.FindById(id);
}
public bool InsertOrUpdate(Member member)
{
// if(member.HomeAddress != null)
// _unitOfWork.SetContextState(member.HomeAddress, EntityState.Modified);
//
// if(member.WorkAddress != null)
// _unitOfWork.SetContextState(member.WorkAddress, EntityState.Modified);
//
// if(member.DontUseHomeAddress)
// {
// //TODO: This is an attempted hack... fix it by moving somewhere (possibly to repository)
// var context = new EfDbContext();
// context.Set<MemberAddress>().Remove(member.HomeAddress);
// context.SaveChanges();
// }
_memberRepository.InsertOrUpdate(member);
return true;
}
}
public class UnitOfWork : IUnitOfWork
{
private readonly EfDbContext _context;
public UnitOfWork()
{
_context = new EfDbContext();
}
public IRepository<T> RepositoryFor<T>() where T : Entity
{
return new Repository<T>(_context);
}
public void Attach(Entity entity)
{
_context.Entry(entity).State = EntityState.Unchanged;
}
public void SetContextState(Entity entity, EntityState state)
{
_context.Entry(entity).State = state;
}
public void Save()
{
_context.SaveChanges();
}
}

Setting the state _context.Entry(entity).State = EntityState.Modified; doesn't affect the state of related entities. If you want to take care of changes of your related entities you must set their state to Modified as well:
if (member.HomeAddress != null)
_context.Entry(member.HomeAddress).State = EntityState.Modified;
if (member.WorkAddress != null)
_context.Entry(member.WorkAddress).State = EntityState.Modified;
_context.Entry(member).State = EntityState.Modified;
This is not generic anymore.
To delete an entity you have to call the appropriate method to delete an entity; setting the navigation property to null is not enough:
_context.MemberAddresses.Remove(member.HomeAddress);

Related

How do you find or know where the "non-owned" entity type when trying to create a migration?

I have the following classes:
JobSeeker which owns a CreditCard which has a CreditCardType
public class JobSeeker : Entity
{
private readonly List<CreditCard> _creditCards;
public IEnumerable<CreditCard> CreditCards => _creditCards.AsReadOnly();
}
public class CreditCard : Entity
{
public CreditCardType CardType { get { return CreditCardType.From(_creditCardTypeID); } private set { } }
private readonly int _creditCardTypeID;}
public class CreditCardType : Enumeration
{
public static readonly CreditCardType Amex = new CreditCardType(1, nameof(Amex).ToLowerInvariant());
public static readonly CreditCardType Visa = new CreditCardType(2, nameof(Visa).ToLowerInvariant());
public static readonly CreditCardType MasterCard = new CreditCardType(3, nameof(MasterCard).ToLowerInvariant());
public static IEnumerable<CreditCardType> List() => new[] { Amex, Visa, MasterCard };}
My DBContext Configs are:
class JobSeekerEntityTypeConfiguration : IEntityTypeConfiguration<JobSeeker>
{
public void Configure(EntityTypeBuilder<JobSeeker> jsConfiguration)
{
if (jsConfiguration == null)
{
throw new ArgumentNullException(nameof(jsConfiguration));
}
// Build the model
jsConfiguration.OwnsOne(s => s.CompleteName);
jsConfiguration.OwnsOne(s => s.HomeAddress);
jsConfiguration.OwnsOne(s => s.BillingAddress);
jsConfiguration.OwnsOne(s => s.EmAddress);
jsConfiguration.OwnsOne(s => s.PersonalPhoneNumber);
jsConfiguration.OwnsMany(a => a.CreditCards);
//jsConfiguration.HasMany<CreditCard>().WithOne(JobSeeker).OnDelete(DeleteBehavior.Restrict);
jsConfiguration.Property<DateTime>("CreatedDate");
jsConfiguration.Property<DateTime>("UpdatedDate");
}
}
class CreditCardTypeEntityTypeConfiguration : IEntityTypeConfiguration<CreditCard>
{
public void Configure(EntityTypeBuilder<CreditCard> ccConfiguration)
{
if (ccConfiguration == null)
{
throw new ArgumentNullException(nameof(ccConfiguration));
}
// Build the model
ccConfiguration.HasOne(o => o.CardType).WithMany().HasForeignKey("_creditCardTypeID");
ccConfiguration.Property<DateTime>("CreatedDate");
ccConfiguration.Property<DateTime>("UpdatedDate");
}
}
class CreditCardEntityTypeConfiguration : IEntityTypeConfiguration<CreditCardType>
{
public void Configure(EntityTypeBuilder<CreditCardType> cctConfiguration)
{
if (cctConfiguration == null)
{
throw new ArgumentNullException(nameof(cctConfiguration));
}
// Build the model
cctConfiguration.ToTable("CreditCardTypes");
cctConfiguration.HasKey(o => o.Id);
cctConfiguration.Property(o => o.Id)
.HasDefaultValue(1)
.ValueGeneratedNever()
.IsRequired();
cctConfiguration.Property(o => o.Name)
.HasMaxLength(200)
.IsRequired();
cctConfiguration.HasData(
new { Id = 1, Name = "Amex" },
new { Id = 2, Name = "Visa" },
new { Id = 3, Name = "MasterCard" });
}
}
My DB Context is:
public class JobSeekerContext : DbContext, IUnitOfWork
{
private static readonly Type[] EnumerationTypes = { typeof(CreditCardType) };
public const string DEFAULT_SCHEMA = "jobseeker";
private readonly ILoggerFactory MyConsoleLoggerFactory;
private readonly IMediator Mediator;
public DbSet<JobSeeker> JobSeekers { get; set; }
public DbSet<CreditCard> CreditCards { get; set; }
public DbSet<CreditCardType> CreditCardTypes { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
if (modelBuilder == null)
{
throw new ArgumentNullException(nameof(modelBuilder));
}
// Build the model
modelBuilder.ApplyConfiguration(new CreditCardTypeEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new CreditCardEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new JobSeekerEntityTypeConfiguration());
}
When I run the migration I get the following error: "The type 'CreditCard' cannot be marked as owned because a non-owned entity type with the same name already exists."
Where is CreditCard marked as non-owned?
Where is CreditCard marked as non-owned?
In JobSeekerContext here
public DbSet<CreditCard> CreditCards { get; set; }
and here
modelBuilder.ApplyConfiguration(new CreditCardTypeEntityTypeConfiguration());
and the whole (misleadingly named) CreditCardTypeEntityTypeConfiguration class since it is IEntityTypeConfiguration<CreditCard>.
Owned entity types are special entities which are configured, queried and updated only through owner.
Here is the excerpt from the By-design restrictions section of the current EF Core documentation:
You cannot create a DbSet<T> for an owned type.
You cannot call Entity<T>() with an owned type on ModelBuilder.
Note that applying IEnityTypeConfiguration<T> class is equivalent of calling Entity<T>() on ModelBuilder.
So you are breaking both aforementioned restrictions.
What you need to do is
Remove the DbSet<CreditCard> property from the context
Remove CreditCardTypeEntityTypeConfiguration class and corresponding ApplyConfiguration call
Move the CreditCard configuration code inside the owner JobSeeker configuration using the builder provided/returned by the OwnsMany method. e.g.
jsConfiguration.OwnsMany(a => a.CreditCards, ccConfiguration =>
{
// Build the model
ccConfiguration.HasOne(o => o.CardType).WithMany().HasForeignKey("_creditCardTypeID");
ccConfiguration.Property<DateTime>("CreatedDate");
ccConfiguration.Property<DateTime>("UpdatedDate");
});

EF Core 3.1: Navigation property doesn't lazy load entities when calling the backing field first

I am using EF Core 3.1.7. The DbContext has the UseLazyLoadingProxies set. Fluent API mappings are being used to map entities to the database. I have an entity with a navigation property that uses a backing field. Loads and saves to the database seem to work fine except for an issue when accessing the backing field before I access the navigation property.
It seems that referenced entities don't lazy load when accessing the backing field. Is this a deficiency of the Castle.Proxy class or an incorrect configuration?
Compare the Student class implementation of IsRegisteredForACourse to the IsRegisteredForACourse2 for the behavior in question.
Database tables and relationships.
Student Entity
using System.Collections.Generic;
namespace EFCoreMappingTests
{
public class Student
{
public int Id { get; }
public string Name { get; }
private readonly List<Course> _courses;
public virtual IReadOnlyList<Course> Courses => _courses.AsReadOnly();
protected Student()
{
_courses = new List<Course>();
}
public Student(string name) : this()
{
Name = name;
}
public bool IsRegisteredForACourse()
{
return _courses.Count > 0;
}
public bool IsRegisteredForACourse2()
{
//Note the use of the property compare to the previous method using the backing field.
return Courses.Count > 0;
}
public void AddCourse(Course course)
{
_courses.Add(course);
}
}
}
Course Entity
namespace EFCoreMappingTests
{
public class Course
{
public int Id { get; }
public string Name { get; }
public virtual Student Student { get; }
protected Course()
{
}
public Course(string name) : this()
{
Name = name;
}
}
}
DbContext
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace EFCoreMappingTests
{
public sealed class Context : DbContext
{
private readonly string _connectionString;
private readonly bool _useConsoleLogger;
public DbSet<Student> Students { get; set; }
public DbSet<Course> Courses { get; set; }
public Context(string connectionString, bool useConsoleLogger)
{
_connectionString = connectionString;
_useConsoleLogger = useConsoleLogger;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddFilter((category, level) =>
category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information)
.AddConsole();
});
optionsBuilder
.UseSqlServer(_connectionString)
.UseLazyLoadingProxies();
if (_useConsoleLogger)
{
optionsBuilder
.UseLoggerFactory(loggerFactory)
.EnableSensitiveDataLogging();
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>(x =>
{
x.ToTable("Student").HasKey(k => k.Id);
x.Property(p => p.Id).HasColumnName("Id");
x.Property(p => p.Name).HasColumnName("Name");
x.HasMany(p => p.Courses)
.WithOne(p => p.Student)
.OnDelete(DeleteBehavior.Cascade)
.Metadata.PrincipalToDependent.SetPropertyAccessMode(PropertyAccessMode.Field);
});
modelBuilder.Entity<Course>(x =>
{
x.ToTable("Course").HasKey(k => k.Id);
x.Property(p => p.Id).HasColumnName("Id");
x.Property(p => p.Name).HasColumnName("Name");
x.HasOne(p => p.Student).WithMany(p => p.Courses);
});
}
}
}
Test program which demos the issue.
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Linq;
namespace EFCoreMappingTests
{
class Program
{
static void Main(string[] args)
{
string connectionString = GetConnectionString();
using var context = new Context(connectionString, true);
var student2 = context.Students.FirstOrDefault(q => q.Id == 5);
Console.WriteLine(student2.IsRegisteredForACourse());
Console.WriteLine(student2.IsRegisteredForACourse2()); // The method uses the property which forces the lazy loading of the entities
Console.WriteLine(student2.IsRegisteredForACourse());
}
private static string GetConnectionString()
{
IConfigurationRoot configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
return configuration["ConnectionString"];
}
}
}
Console Output
False
True
True
When you declare a mapped property in an EF entity as virtual, EF generates a proxy which is capable of intercepting requests and assessing whether the data needs to be loaded. If you attempt to use a backing field before that virtual property is accessed, EF has no "signal" to lazy load the property.
As a general rule with entities you should always use the properties and avoid using/accessing backing fields. Auto-initialization can help:
public virtual IReadOnlyList<Course> Courses => new List<Course>().AsReadOnly();

Get identity value after insert in entity framework while using dependancy injection

I am using entity frame work in my mvc core application. I am also using dependency injection technique. Now i want to get the value of new record identity column. I am using the code as..
public interface IGenericRepositoryStudent<T> where T : class
{
void Add(T item);
}
public class GenericRepositoryStudent<T> : IGenericRepositoryStudent<T> where T : class
{
private eerp_studentContext _context;
private DbSet<T> _dbSet;
public GenericRepositoryStudent(eerp_studentContext context)
{
_context = context;
_dbSet = _context.Set<T>();
}
public void Add(T item)
{
_dbSet.Add(item);
_context.SaveChanges();
}
}
Controller:
public class StudentController : Controller
{
private IStudentRepository _dbSet;
public StudentController(IStudentRepository dbSet)
{
_dbSet = dbSet;
}
public long Add([FromBody]Student _student)
{
if (ModelState.IsValid)
{
_dbSet.Add(acJournal);
long _id = _student.id; // value of _id is always 0.
return _id;
}
}
}
Apparently every T that you want to Add has a Primary Key. You want the value of this primary Key after the T has been added and changes are saved.
This means, that you can't add objects of every class, you can only add classes that have a Primary Key.
The easiest, and a very type safe way (checked by compiler) is to allow only adding of object that have a primary key.
// interface of objects that have Id (primary key)
public interface IID
{
public long Id {get; set;}
}
If your database uses another type for primary keys like int, or GUID, use this other type as return type
public interface IGenericRepositoryStudent<T> where T : IID
{
T Add(T item);
}
public class GenericRepositoryStudent<T> : IGenericRepositoryStudent<T> where T : IID
{
...
public T Add(T itemToAdd)
{
T addedItem = _dbSet.Add(item);
_context.SaveChanges();
return addedItem
}
}
Usage:
class Student : IID
{
public long Id {get; set;} // primary key
...
}
public class StudentController : Controller
{
...
public T Add([FromBody]Student _student)
{
if (ModelState.IsValid)
{
return _dbSet.Add(_student);
}
}
}
I chose to return the complete T instead of only the Id, because if you need some of the properties of the added item, you don't need to fetch the item you just added. Besides this is just the return value of DbSet.Add(T)
If you really want, just return the Student's Id.

Seeding not working in Entity Framework Code First Approach

I am developing a .Net project. I am using entity framework code first approach to interact with database. I am seeding some mock data to my database during development. But seeding is not working. I followed this link - http://www.entityframeworktutorial.net/code-first/seed-database-in-code-first.aspx.
This is my ContextInitializer class
public class ContextInitializer : System.Data.Entity.CreateDatabaseIfNotExists<StoreContext>
{
protected override void Seed(StoreContext context)
{
IList<Brand> brands = new List<Brand>();
brands.Add(new Brand { Name = "Giordano" ,TotalSale = 1 });
brands.Add(new Brand { Name = "Nike" , TotalSale = 3 });
foreach(Brand brand in brands)
{
context.Brands.Add(brand);
}
base.Seed(context);
context.SaveChanges();
}
}
This is my context class
public class StoreContext : DbContext,IDisposable
{
public StoreContext():base("DefaultConnection")
{
Database.SetInitializer(new ContextInitializer());
}
public virtual DbSet<Category> Categories { get; set; }
public virtual DbSet<Brand> Brands { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
}
}
This is my brand class
public class Brand
{
public int Id { get; set; }
[Required]
[MaxLength(40)]
public string Name { get; set; }
public int TotalSale { get; set; }
}
I searched solutions online and I followed instructions. I run context.SaveChanges as well. But it is not seeding data to database. Why it is not working?
You are taking the wrong initializer, CreateDatabaseIfNotExists is called only if the database not exists!
You can use for example DropCreateDatabaseIfModelChanges:
Solution 1)
public class ContextInitializer : System.Data.Entity.DropCreateDatabaseIfModelChanges<StoreContext>
{
You have to take care with this approach, it !!!removes!!! all existing data.
Solution 2)
Create a custom DbMigrationsConfiguration:
public class Configuration : DbMigrationsConfiguration<StoreContext>
{
public Configuration()
{
// Take here! read about this property!
this.AutomaticMigrationDataLossAllowed = true;
this.AutomaticMigrationsEnabled = false;
}
protected override void Seed(StoreContext context)
{
IList<Brand> brands = new List<Brand>();
brands.Add(new Brand { Name = "Giordano", TotalSale = 1 });
brands.Add(new Brand { Name = "Nike", TotalSale = 3 });
foreach (Brand brand in brands)
{
context.Brands.AddOrUpdate(m => m.Name, brand);
}
base.Seed(context);
context.SaveChanges();
}
}
In this way you can called( !!Before you create the DbContext or in the DbContext constructor!!):
// You can put me also in DbContext constuctor
Database.SetInitializer(new MigrateDatabaseToLatestVersion<StoreContext , Yournamespace.Migrations.Configuration>("DefaultConnection"));
Notes:
DbMigrationsConfiguration need to know about the connection string you can provide this info in the constructor or from outside.
In Your DbMigrationsConfiguration you can configure also:
MigrationsNamespace
MigrationsAssembly
MigrationsDirectory
TargetDatabase
If you leave everything default as in my example then you do not have to change anything!
Setting the Initializer for a Database has to happen BEFORE the context is ever created so...
public StoreContext():base("DefaultConnection")
{
Database.SetInitializer(new ContextInitializer());
}
is much to late. If you made it static, then it could work:
static StoreContext()
{
Database.SetInitializer(new ContextInitializer());
}
Your code is working if you delete your existing database and the EF will create and seeding the data
Or
You can use DbMigrationsConfiguration insted of CreateDatabaseIfNotExists and change your code as follow:
First you have to delete the existing database
ContextInitializer class
public class ContextInitializer : System.Data.Entity.Migrations.DbMigrationsConfiguration<StoreContext>
{
public ContextInitializer()
{
this.AutomaticMigrationDataLossAllowed = true;
this.AutomaticMigrationsEnabled = true;
}
protected override void Seed(StoreContext context)
{
IList<Brand> brands = new List<Brand>();
brands.Add(new Brand { Name = "Giordano", TotalSale = 1 });
brands.Add(new Brand { Name = "Nike", TotalSale = 3 });
foreach (Brand brand in brands)
{
context.Brands.AddOrUpdate(m => m.Name, brand);
}
base.Seed(context);
context.SaveChanges();
}
}
StoreContext
public class StoreContext : DbContext, IDisposable
{
public StoreContext() : base("DefaultConnection")
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<StoreContext, ContextInitializer>());
// Database.SetInitializer(new ContextInitializer());
}
public virtual DbSet<Category> Categories { get; set; }
public virtual DbSet<Brand> Brands { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
}
}
Then any change in your seed will automatically reflected to your database

EF Context not keeping values after adding entity

Edit Is this post lacking sufficient information to get some guidance?
I have this method to insert an entity into the database:
public void Insert(T entity)
{
_context.Set<T>().Add(entity);
_context.SaveChanges();
}
When I inspect entity before adding it to the context, my CustomerRole field is there. Once the add has taken place, the context doesn't seem to have it. Because of this, I am receiving this error:
Entities in 'CcDataContext.Customers' participate in the
'Customer_CustomerRole' relationship. 0 related
'Customer_CustomerRole_Target' were found. 1
'Customer_CustomerRole_Target' is expected.
These images show what I mean:
Inspecting my entity
Inspecting the context
Can anyone explain this behaviour and what I can do about it?
This is the structure of my classes (cut down for brevity):
public class Customer : BaseEntity
{
public CustomerRole CustomerRole { get; set; }
}
class CustomerMap : EntityTypeConfiguration<Customer>
{
public CustomerMap()
{
HasRequired(t => t.CustomerRole)
.WithMany(t => t.Customers);
}
}
public class CustomerRole : BaseEntity
{
private ICollection<Customer> _customers;
public ICollection<Customer> Customers
{
get { return _customers ?? (new List<Customer>()); }
set { _customers = value; }
}
}
I can confirm that customer map is being added to the configuration and my database is built in line with them.
This is the call I am making which does the insert:
public Customer InsertGuestCustomer()
{
var customer = new Customer();
CustomerRole guestRole = GetCustomerRoleByName("Guest");
if (guestRole == null)
throw new Exception("Customer Role is not defined!");
customer.UserName = "";
customer.EmailAddress = "";
customer.Password = "";
customer.IsAdmin = false;
customer.CustomerRole = guestRole;
_customerRepository.Insert(customer);
return customer;
}
I have no other data in my database, this would be the first customer record and only one CustomerRole. My Customer table has a Foreign Key pointing to my CustomerRole.Id table / column.
Mark your navigation properties as virtual and initialize the collection property in the entity constructor rather than from the property getter.
public class Customer : BaseEntity
{
public virtual CustomerRole CustomerRole { get; set; }
}
...
public class CustomerRole : BaseEntity
{
public CustomerRole()
{
Customers = new List<Customer>();
}
public virtual ICollection<Customer> Customers { get; protected set; }
}
In your Customers property, you were returning a new List in the getter when the backing field was null, but you never assigned this to your backing field.