0

So I have for example these models:

public class Account 
{
    public Guid ID {get;set;}

    [ForeignKey("Profile")]
    public Guid? ProfileId {get;set;}
    public virtual Profile Profile {get;set;}
    ......
}

public class Profile
{
    public Guid ID {get;set;}

    [ForeignKey("Account")]
    public Guid AccountId {get;set;}
    public virtual Account Account {get;set;}
}

Then I have a view (html) which request from a controller.

public class ExController : ApiController
{
    public async Task<Account> GetAccount(Guid id)
    {
        return await db.Accounts.Where(m => m.ID == id).FirstOrDefaultAsync();
    }
}

Then the html view edit / update the account entity, then pass it again to controller to be saved into database. If I just save from the raw passed entity from the view, the Profile will be treated as new entity. This is not what I want, I want the Account and Account.Profile to be updated, not added.

For example:

public async Task UpdateModel(Account model)
{
    db.Accounts.Attach(model);
    db.Entry(model).State = EntityState.Modified;
    await db.SaveChangesAsync();
}

The child profile inside model will be treated as new entity.

How can I update the children entities as well?


EDIT

So my purpose is not using entity as view model. I am developing an object mapper which map all properties from vm to model and vise versa.

Something like this:

vm.MapTo<Model>(ref existing);

The problem is EF always sees the children entity mapped from view model as new entities.


EDIT

This is my mapping function

    public void MapTo<T>(ref T instance)
    {
        var fromProperties = this.GetType().GetProperties();
        var toProperties = typeof(T).GetProperties();

        foreach (PropertyInfo prop in fromProperties)
        {
            string propName = prop.Name;
            if (prop.CustomAttributes.Count() > 0)
            {
                MapToAttribute mapAttribute = (MapToAttribute)Attribute.GetCustomAttribute(prop, typeof(MapToAttribute));
                AutoGeneratedAttribute autoGeneratedAttribute = (AutoGeneratedAttribute)Attribute.GetCustomAttribute(prop, typeof(AutoGeneratedAttribute));

                if (mapAttribute != null)
                    propName = mapAttribute.MappedProperty;

                if (autoGeneratedAttribute != null)
                    propName = "";
            }
            PropertyInfo mappedProp = toProperties.Where(m => m.Name == propName).FirstOrDefault();

            if (mappedProp != null)
            {
                object value = prop.GetValue(this);
                if (value != null && value.GetType() == typeof(DateTime))
                {
                    DateTime dateValue = ((DateTime)(value)).ToUniversalTime();
                    mappedProp.SetValue(instance, dateValue);
                }
                else if (value != null && value.GetType() == typeof(DateTime?))
                {
                    DateTime? dateValue = ((DateTime?)(value)).Value.ToUniversalTime();
                    mappedProp.SetValue(instance, dateValue);
                }
                else
                {
                    mappedProp.SetValue(instance, value);
                }
            }
        }
    }
Alvin Stefanus
  • 1,163
  • 13
  • 31
  • 1
    You need to re-attach all of those with `Attach` just like your `Account`. – Dai Dec 11 '20 at 07:15
  • But **you should stop what you're doing immediately** because **YOU SHOULD NOT USE EF ENTITY CLASSES AS VIEW-MODEL CLASSES!!** - it's almost as bad as SQL Injection because users can craft custom `
    ` submissions with arbitrary data and overwrite your FK keys, for example. I could edit the `AccountId` value in the `
    ` and edit someone else's Account! Always use dedicated and sanitized viewmodels and DTOs.
    – Dai Dec 11 '20 at 07:16
  • Another reason not to use EF Entity Classes as DTOs or View Models is because ASP.NET and Entity Framework use different Data Annotation attributes - or they reuse the same attributes but for different purposes (e.g. When EF `[Required]` it's to restrict SQL `NULL`s but not empty-strings, whereas ASP.NET uses `[Required]` to prevent empty-string values. And so on... – Dai Dec 11 '20 at 07:18
  • Hello @Dai, I am not using view model as entity. In my case, I am creating an object mapper from view model to model. I want to automate the variable assignment from vm to model. But the problem is when I assigning children entities, the EF sees the children as new entity every time. – Alvin Stefanus Dec 11 '20 at 07:19
  • "I am not using view model as entity." but **you are** using an entity class as a view-model because you're directly passing your `Account` object to the View and you're having ASP.NET perform Form Model Binding on it by using it as a controller action parameter. It's a terrible excuse that you think you're saving time because you're skipping writing a mapping (that's what `AutoMapper` is for) - you're introducing **massive security holes** to your project. – Dai Dec 11 '20 at 07:20
  • I appreciate your comment, but please this is not what I am asking, you are off the track. – Alvin Stefanus Dec 11 '20 at 07:23
  • can you explain more what kind of `object mapper` you use? did you define mapping properly ? Are children loaded properly? Lazy loading will not work out of scope of dbcontext – Pribina Dec 11 '20 at 07:30
  • I am developing my own object mapper, In debug, all the properties are mapped properly. Only when I called `SaveChanges` it throws error like this `Violation of PRIMARY KEY constraint 'PK_dbo.ItemTypes'. Cannot insert duplicate key in object 'dbo.ItemTypes'. The duplicate key value is (19e80f17-01bc-4980-a8a1-ea0619351334). The statement has been terminated.`. EF is trying to insert the children as new entity. – Alvin Stefanus Dec 11 '20 at 07:35
  • https://stackoverflow.com/questions/7968598/entity-4-1-updating-an-existing-parent-entity-with-new-child-entities/7969372#7969372 ef doesnt know that children are also updated just because parent is – Pribina Dec 11 '20 at 07:48
  • You should set the Profile.Id values to their original values in the database. – Gert Arnold Dec 11 '20 at 08:10

1 Answers1

0

be careful with accepting model from UI. Its not good practice. This way you choose what is stored and also you can validate it.

query item again from db

public async Task UpdateModel(Account model)
{
    var dbItem = await db.Accounts.Include(x=>x.Profile).FirstOrDefaultAsync(m => m.ID == model.Id);

    //map values from model to dbItem 

    await db.SaveChangesAsync();
}
Pribina
  • 540
  • 1
  • 5
  • 13