RobVious
RobVious

Reputation: 12915

Partial Updates for Entities with Repository/DTO patterns in MVC (prepping for API)

I've built my Domain model layer, my repository layer, and now I'm working on my DTO layer to be used by a webApi project. I'm in the middle of implementing an Update service method, and I'm wondering about partial updates. Here's my DTO class:

public class FullPersonDto
    {
        public FullPersonDto()
        {
            Friends = new List<Person>();
        }

        public FullPersonDto(Person person)
        {
            PersonId = person.PersonId;
            DateCreated = person.DateCreated;
            Details = person.Details;
            Friends = new List<Person>();
            foreach (Person friend in person.Friends)
            {
                Friends.Add(new PersonDto(friend));
            }
        }

        [Key]
        public int PersonId { get; set; }

        [Required]
        public virtual DateTime DateCreated { get; set; }

        public virtual string Details { get; set; }

        public List<Person> Friends { get; set; } 

        public Person ToEntity()
        {
            var person = new Person
            {
                PersonId = PersonId,             
                DateCreated = (DateTime) DateCreated,
                Details = Details,
                Friends = new List<Person>()
            };
            foreach (PersonDto friend in Friends)
            {
                person.Friends.Add(friend.ToEntity());
            }
            return person;
        }
    }

Here's my Update method in my Repository:

public Person UpdatePerson(Person person)
        {
            var entry = _db.Entry(person);
            if (entry.State == EntityState.Detached)
            {
                var dbSet = _db.Set<Person>();
                Person attachedPerson = dbSet.Find(person.PersonId);
                if (attachedPerson != null)
                {
                    var attachedEntry = _db.Entry(attachedPerson);
                    attachedEntry.CurrentValues.SetValues(person);  // what if values are null, like ID, or DateCreated?
                }
                else
                {
                    entry.State = EntityState.Modified;
                }

            }
            SaveChanges();
            return person;
        }

My question is: What if I only need to update the Details of a person via my webAPI? Is the convention to construct an entire PersonDto and Update the entire object using SetValues, or is there any way I can specify that I only want a single field updated so that I don't have to send a ton of data over the wire (that I don't really need)?

If it is possible to do partial updates, when is it ever good to update the entire entity? Even if I have to update 5/7 properties, it requires that I send old data for 2/7 to re-write so that SetValues doesn't write nulls into my fields from my DTO.

Any help here would be awesome... totally new to this stuff and trying to learn everything right. Thank you.

Upvotes: 4

Views: 2634

Answers (2)

Admir Tuzović
Admir Tuzović

Reputation: 11177

I've taken similar approach to do optimization, and I've faced same issues with null values when attaching (not just null, you'll have issue with boolean as well). This is what I've come up with:

    public static void Update<T>(this DbContext context, IDTO dto)
        where T : class, IEntity
    {
        T TEntity = context.Set<T>().Local.FirstOrDefault(x => x.Id == dto.Id);
        if (TEntity == null)
        {
            TEntity = context.Set<T>().Create();
            TEntity.Id = dto.Id;
            context.Set<T>().Attach(TEntity);
        }
        context.Entry(TEntity).CurrentValues.SetValues(dto);
        var attribute = dto.GetAttribute<EnsureUpdatedAttribute>();
        if (attribute != null)
        {
            foreach (var property in attribute.Properties)
                context.Entry(TEntity).Property(property).IsModified = true;
        }
    }

That is extension method for DbContext. Here are the interfaces IDTO and IEntity:

public interface IDTO
{
    int Id { get; set; }
}

public interface IEntity
{
    int Id { get; set; }

    Nullable<DateTime> Modified { get; set; }
    Nullable<DateTime> Created { get; set; }
}

I'm using my custom EnsureUpdatedAttribute to annotate what properties should always be updated (to deal with nulls / default values not being tracked):

public class EnsureUpdatedAttribute : Attribute
{
    public IEnumerable<string> Properties { get; private set; }

    public EnsureUpdatedAttribute(params string[] properties)
    {
        Properties = properties.AsEnumerable();
    }
}

And this is a sample of usage:

public class Sample : IEntity
{
    public int Id { get; set; }

    public string Name { get; set; }

    public bool Active { get; set; }

    public Nullable<DateTime> Modified { get; set; }
    public Nullable<DateTime> Created { get; set; }
}


[EnsureUpdated("Active")] /// requirement for entity framework change tracking, read about stub entities
public class SampleDTO : IDTO
{
    public int Id { get; set; }

    [Required]
    public string Name { get; set; }

    [JsonIgnore] /// How to exclude property from going on the wire / ignored for serialization
    public bool Active { get; set; }
}

    [HttpPost]
    public HttpResponseMessage SaveSample(SampleDTO dto)
    {
        dto.Active = true;
        _ctx.AddModel<Sample>(dto);
        _ctx.SaveChanges();
        return NoContent();
    }

return NoContent() is just extension for returning 204 (NoContent).

Hope this helps.

Upvotes: 2

gdp
gdp

Reputation: 8242

Theres a few options you have, you can create a stored procedure to update the required parts (I wouldnt do this), or you can manually select the fileds to update on the model before saving the context changes with EF.

Heres an example how to update a specific field:

public void UpdatePerson(int personId, string details)
{
  var person = new Person() { Id = personId, Details = details };   
  db.Persons.Attach(personId);
  db.Entry(person).Property(x => x.Details).IsModified = true;
  db.SaveChanges();
}

It will depend on your scenario what you want to do, but generally speaking its fine to send your whole entity to be updated, and this is how i would approach your situation potentially changing in the future if needed.

Upvotes: 0

Related Questions