Reputation: 387
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
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
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
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