jag
jag

Reputation: 387

Generic Update method that avoids mass assignment

How do I create a generic update method in my generic repository that avoids the mass assignment problem by specifying which properties to update?

Currently I have separate update methods for each of my 'non-generic' repositories, where i create a new entity based on id, then manually assign each property. See code below for example.

    public OperationStatus DoUpdateGame(Game game)
    {
        var gm = DataContext.Games.Where(g => g.Id == game.Id).FirstOrDefault();

        if (gm != null)
        {
            //manually map all properties to avoid mass assignment security issues
            gm.CreatorId = game.CreatorId;
            gm.CurrentOrderPosition = game.CurrentOrderPosition;
            gm.HasStarted = game.HasStarted;
            gm.Name = game.Name;
            gm.HasEnded = game.HasEnded;
            gm.WinnerId = game.WinnerId;
        }
        return new OperationStatus { Status = true };
    }

I want to get rid of the duplication (separate update methods for each repository) & have a single update method in my generic repository. To make this generic, I need to

  1. Know how to create a new, generic entity 'copy'
  2. Use a list of strings to mark which properties to update

I'm trying to do something like this (currently doesn't work). Guidance appreciated!

public virtual OperationStatus Update(T entity, string idColName, params string[] propsToUpdate) 
    {
        OperationStatus opStatus = new OperationStatus { Status = true };

        // this line fails with 'LINQ to Entities does not recognize the method 'System.Reflection.PropertyInfo GetProperty(System.String)' method, and this method cannot be translated into a store expression.'
        var current = DataContext.Set<T>().Where(g => g.GetType().GetProperty(idColName) == entity.GetType().GetProperty(idColName)).FirstOrDefault();

        try
        {
            if (propsToUpdate != null && propsToUpdate.Length > 0)
            {
                //update specific properties
                foreach (var propName in propsToUpdate)
                {
                    TrySetProperty(current, propName, entity.GetType().GetProperty(propName));
                }
            }

            opStatus.Status = true; // DataContext.SaveChanges() > 0;
        }
        catch (Exception exp)
        {
            // log
        }

        return opStatus;
    }

    private void TrySetProperty(object obj, string property, object value)
    {
        var prop = obj.GetType().GetProperty(property, BindingFlags.Public | BindingFlags.Instance);
        if (prop != null && prop.CanWrite)
            prop.SetValue(obj, value, null);
    }

UPDATE

I'm now tyring to use automapper with a DTO object to map the original entity to the DTO. Am now getting a 'no items in sequence error'... code below

public virtual OperationStatus Update(T entity, string idColName, params string[] propsToUpdate) 
    {
        OperationStatus opStatus = new OperationStatus { Status = true };

        var sourceType = entity.GetType();

        // this line gives the 'no items in sequence error
        var destinationType = Mapper.GetAllTypeMaps().
          Where(map => map.SourceType == sourceType).
          Single().DestinationType;

        var current = Mapper.Map(entity, destinationType);

My automapperconfiguration

public static class AutoMapperConfiguration
{
    public static void Configure()
    {
        ConfigureMapping();
    }

    private static void ConfigureMapping()
    {
        Mapper.CreateMap<Game, GameUpdateDTO>();
    }
}

Line in global.asax

            AutoMapperConfiguration.Configure();

DTO model

 class GameUpdateDTO
{
    public int Id { get; set; }
    public int CreatorId { get; set; }
    public string Name { get; set; }
    public int CurrentOrderPosition { get; set; }
    public bool HasStarted { get; set; }
    public bool HasEnded { get; set; }
    public int WinnerId { get; set; }
}

Upvotes: 0

Views: 1306

Answers (2)

Slauma
Slauma

Reputation: 177153

Once you have a DTO class there is an easy way for a generic update that is built in into Entity Framework:

public void Update<T>(object dto, Expression<Func<T, bool>> currentEntityFilter)
    where T : class
{
    var current = DataContext.Set<T>().FirstOrDefault(currentEntityFilter);
    DataContext.Entry(current).CurrentValues.SetValues(dto);
}

It's important that the DTO properties have the same names as the entity properties.

You can call it like so:

Update<Game>(gameUpdateDto, g => g.Id == gameUpdateDto.Id);

Or if you just want to update a few selected properties, like so:

Update<Game>(new
{
    gameUpdateDto.Name,
    gameUpdateDto.HasStarted,
    gameUpdateDto.HasEnded
}, g => g.Id == gameUpdateDto.Id);

If you always want to update an entity by its key I would prefer this signature and implementation:

public void Update<T>(object dto, params object[] keyValues)
    where T : class
{
    var current = DataContext.Set<T>().Find(keyValues);
    DataContext.Entry(current).CurrentValues.SetValues(dto);
}

To be called like so:

Update<Game>(gameUpdateDto, gameUpdateDto.Id);

Note that this does not work if you want to update an entity including its navigation properties to related entities.

Upvotes: 1

Hossain Muctadir
Hossain Muctadir

Reputation: 3626

You can use AutoMapper to solve this issue very easily. Here are some code that might give you some idea. I find the link very helpful. You might want to have a look there.

Mapper.CreateMap<Game, Game>();
Mapper.DynamicMap(newGame, oldGame);
GameRepository.Update(oldGame);

Update Try this example for generic type

internal class Program
    {
        private static void Main(string[] args)
        {
            var customerRepo = new Repository<Customer>(new Customer());
            var userRepo = new Repository<User>(new User());

            customerRepo.Update(new Customer
                                    {
                                        DateOfBirth = DateTime.Now,
                                        FirstName = "Customer",
                                        LastName = "Cust",
                                        NumberOfOrders = 3
                                    });
            userRepo.Update(new User
                                {
                                    DateOfBirth = DateTime.Now,
                                    FirstName = "User",
                                    LastName = "Us"
                                });
            userRepo.Show();
            customerRepo.Show();
            Console.ReadKey();
        }


        public class Repository<T>
        {
            private T _entity;

            public Repository(T entity)
            {
                _entity = entity;
            }

            public void Update(T data)
            {
                Mapper.CreateMap<T, T>();
                Mapper.DynamicMap(data, _entity);
            }

            public void Show()
            {
                Console.WriteLine(_entity.ToString());
            }
        }

        public class Customer
        {
            public string FirstName { get; set; }
            public string LastName { get; set; }
            public DateTime DateOfBirth { get; set; }

            public int NumberOfOrders { get; set; }

            public override string ToString()
            {
                return GetType().Name + "= " + FirstName + " " + LastName + " " + DateOfBirth + " " + NumberOfOrders;
            }
        }

        public class User
        {
            public string FirstName { get; set; }
            public string LastName { get; set; }
            public DateTime DateOfBirth { get; set; }

            public override string ToString()
            {
                return GetType().Name + "= " + FirstName + " " + LastName + " " + DateOfBirth;
            }
        }
    }

Upvotes: 1

Related Questions