Alberto
Alberto

Reputation: 50

.NET Framework & Entity Framework : not saving entity's nested collection related with foreign key

I'm having some issues with Entity Framework on .NET Framework. My app is designed as such:

I have my class that inherits from DbContext and I have this property:

public DbSet<User> Users { get; set; }
// ...other properties

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
   base.OnModelCreating(modelBuilder);

   modelBuilder.Entity<UserManageableZones>()
       .HasRequired(umz => umz.User) 
       .WithMany(u => u.ManageableZones)
       .HasForeignKey(umz => umz.Username)
       .WillCascadeOnDelete(false);
}

My User model is something like this:

[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Required]
[MaxLength(128)]
[Column("USERNAME")]
public string Username { get; set; } = string.Empty;

public virtual ICollection<UserManageableZones> ManageableZones { get; set; }

public User()
{
    ManageableZones = new HashSet<UserManageableZones>();
}

As you can see, I have the property ManageableZones, that is the table I want to relate with, with a 1-to-many relationship. So basically one user can have many UserManageableZones (for simplicity's sake, a zone can be related with only 1 user, logically this is incorrect, but I don't care about having a many-to-many relationship).

This is my UserManageableZones:

public virtual User User { get; set; }

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

[ForeignKey("User")]
[Required]
[Column("USERNAME")]
public string Username { get; set; }

// ...other properties

Then, I have a class that is basically a projection of the data from my database:

public Class DataContext
{
    public List<User> Users {get; set;}

    public DataContext()
    {
        Users = new List<User>();
    }
}

DataContext is the "static" object with which I'll interact with data, adding/deleting/updating and so on. Then I'll save this data back to the database.

To do so, I have a DataAccess class, that basically inits the DataContext by populating all its properties with database's data, and updating database data with the one from the DataContext.

To save the data into the database, I have this, IMO, horrible method that unfortunately I didn't really understand how it works, so I didn't really ever touch it

private void SaveTable<T1, T2>(List<T1> entity, bool canUpdate = true, bool canDelete = true)
    where T1 : class
{
    PrintCallStack(false);

    try
    {
        AMMContext db;

        if (AMMContext.DbType == Entity.eDbType.SqLite)
            db = new AMMContext(AMMContext.LoadConnectionString());
        else
            db = new AMMContext();

        using (db)
        {
            List<T1> listInDB = (List<T1>)Activator.CreateInstance(typeof(List<T1>));
            DbSet<T1> dbSet = null;

            foreach (PropertyInfo pInfo in typeof(AMMContext).GetProperties())
            {
                if (pInfo.GetValue(db) != null)
                {
                    if (pInfo.GetValue(db).GetType() == typeof(DbSet<T1>))
                    {
                        dbSet = (DbSet<T1>)pInfo.GetValue(db);
                        listInDB = dbSet.ToList();
                    }
                }
            }

            List<T2> idEntityList = null;

            if (entity != null)
                idEntityList = entity.Select(y => (T2)GetPrimaryKeyValue(y)).ToList();

            //List<T1> updating = entity.Where(x => idListinDb.Contains((T2)GetPrimaryKeyValue(x))).ToList();
            //List<T1> adding = entity.Where(x => !idListinDb.Contains((T2)GetPrimaryKeyValue(x))).ToList();

            if (canUpdate)
                dbSet.AddOrUpdate(entity.ToArray());

            if (canDelete)
            {
                List<T1> deleting = dbSet.ToList().Where(x => !idEntityList.Contains((T2)GetPrimaryKeyValue(x))).ToList();
                deleting.ForEach(x => dbSet.Attach(x));
                dbSet.RemoveRange(deleting);
            }

            db.SaveChanges();
        }
    }
    catch (Exception e)
    {
        try
        {
            PrintCallStack(true);
            Log.ErrorFormat($"DataAccess.Save error, transaction rollback occurred | exc:{e.InnerException.InnerException.Message}");
        }
        catch (Exception ex)
        {
            Log.ErrorFormat($"DataAccess.Save error, rollback procedure failure");
            StringBuilder sb = new StringBuilder();
            sb.Append("For entity type : ");

            if (entity != null)
            {
                foreach (T1 item in entity)
                {
                    sb.Append(item.GetType().Name);
                    sb.Append(" - ");
                }
            }

            Log.ErrorFormat(sb.ToString());
        }
    }
}

This method is called by the DataAccess all around the code, wrapped inside another method that does something like this

public void Save(bool allFilter = true, bool usersFilter = false, ...other filters)
{
        if (ammUsersFilter | allFilter)
            SaveTable<User, string>(entity: DataContext.Users, canUpdate: true, canDelete: !initialConditions);

       // ...same for other filters
}

That's about it. The problem is that the collection of UserManageableZones inside User, it's not getting saved in its table. As it is right now, when the app starts from scratch, the database is created and also the table USER_ZONES is created.

I've tried to manually create a record inside the USER_ZONES table, then boot up the app and saw that when the data is gathered from the database, I actually have that record inside the user. This makes me think that relation wise, my work is correct. But I have no clue about what I'm missing.

Ah, just to mention. Inside my USER table, I have no column related to USER_ZONES, but I think it's correct. That's it.

These are the script generated of both tables

CREATE TABLE AMM_USER 
(
    USERNAME VARCHAR(128) NOT NULL,
    ...
    CONSTRAINT PK_AMM_USER PRIMARY KEY (USERNAME)
);

CREATE TABLE USER_ZONES 
(
    ID INTEGER NOT NULL,
    USERNAME VARCHAR(128) NOT NULL,
    CONSTRAINT PK_USER_ZONES PRIMARY KEY (ID),
    CONSTRAINT FK_USER_ZONES_AMM_USER_USERNAME FOREIGN KEY (USERNAME) REFERENCES AMM_USER(USERNAME)
);

CREATE INDEX FK_USER_ZONES_AMM_USER_USERNAME ON USER_ZONES (USERNAME);
CREATE INDEX IX_USER_ZONES_USERNAME ON USER_ZONES (USERNAME);

Upvotes: 0

Views: 34

Answers (0)

Related Questions