C# 在实体框架中保存实体的通用方法

C# 在实体框架中保存实体的通用方法,c#,entity-framework,repository-pattern,C#,Entity Framework,Repository Pattern,我正在尝试编写一个GenericEFRepository,它将被其他存储库使用。我有一个保存方法如下 public virtual void Save(T entity) // where T : class, IEntity, new() And IEntity enforces long Id { get; set; } { var entry = _dbContext.Entry(entity); if (entry.State != EntityState.Detach

我正在尝试编写一个GenericEFRepository,它将被其他存储库使用。我有一个保存方法如下

public virtual void Save(T entity) // where T : class, IEntity, new() And IEntity enforces long Id { get; set; }
{
    var entry = _dbContext.Entry(entity);

    if (entry.State != EntityState.Detached)
        return; // context already knows about entity, don't do anything

    if (entity.Id < 1)
    {
        _dbSet.Add(entity);
        return;
    }

    var attachedEntity = _dbSet.Local.SingleOrDefault(e => e.Id == entity.Id);
    if (attachedEntity != null)
        _dbContext.Entry(attachedEntity).State = EntityState.Detached;
    entry.State = EntityState.Modified;
}
public virtual void Save(T entity)//其中T:class、IEntity、new()和IEntity强制执行长Id{get;set;}
{
var entry=_dbContext.entry(实体);
if(entry.State!=EntityState.Detached)
return;//上下文已经知道实体,不做任何操作
如果(实体Id<1)
{
_添加(实体);
返回;
}
var attachedEntity=_dbSet.Local.SingleOrDefault(e=>e.Id==entity.Id);
如果(附件身份!=null)
_Entry(AttacheIdentity).State=EntityState.Distached;
entry.State=EntityState.Modified;
}
您可以在下面代码的注释中找到问题

 using (var uow = ObjectFactory.GetInstance<IUnitOfWork>()) // uow is implemented like EFUnitOfWork which gives the DbContext instance to repositories in GetRepository
 {
    var userRepo = uow.GetRepository<IUserRepository>();

    var user = userRepo.Get(1);
    user.Name += " Updated";

    userRepo.Save(user);
    uow.Save(); // OK only the Name of User is Updated 
 }

 using (var uow = ObjectFactory.GetInstance<IUnitOfWork>())
 {
    var userRepo = uow.GetRepository<IUserRepository>();

    var user = new User 
    {
        Id = 1,
        Name = "Brand New Name"
    };

    userRepo.Save(user);
    uow.Save();

    // NOT OK
    // All fields (Name, Surname, BirthDate etc.) in User are updated
    // which causes unassigned fields to be cleared on db
 }
using(var-uow=ObjectFactory.GetInstance())//uow的实现类似于EFUnitOfWork,它将DbContext实例提供给GetRepository中的存储库
{
var userRepo=uow.GetRepository();
var user=userRepo.Get(1);
user.Name+=“已更新”;
userRepo.Save(用户);
uow.Save();//确定仅更新用户名
}
使用(var uow=ObjectFactory.GetInstance())
{
var userRepo=uow.GetRepository();
var user=新用户
{
Id=1,
Name=“全新名称”
};
userRepo.Save(用户);
uow.Save();
//不好
//更新用户中的所有字段(姓名、姓氏、出生日期等)
//这会导致在db上清除未分配的字段
}
我能想到的唯一解决方案是通过存储库创建实体,如
userRepo.CreateEntity(id:1)
,存储库将返回一个附加到DbContext的实体。但这似乎容易出错,但任何开发人员都可以使用
new
关键字创建实体

你对这个问题有什么解决建议


注意:我已经知道使用GenericRepository和IEntity接口的利弊。所以,“不要使用GenericRepository,不要使用IEntity,不要在每个实体中都放一个长Id,不要做你想做的事情”的评论不会有帮助。

这是这种方法的一个基本问题,因为你希望存储库神奇地知道你更改了哪些字段,哪些字段没有更改。如果
null
是有效值,则使用
null
作为“未更改”的信号不起作用

您需要告诉存储库您想要编写哪些字段,例如发送带有字段名的
字符串[]
。或者每个字段一个布尔值。我认为这不是一个好的解决办法

也许您可以像这样反转控制流:

var entity = repo.Get(1);
entity.Name += "x";
repo.SaveChanges();
这将允许更改跟踪工作。它更接近EF希望使用的方式

备选方案:

var entity = repo.Get(1);
entity.Name += "x";
repo.Save(entity);

是的,它很容易出错,但这就是EF和存储库的问题所在。在设置任何要更新的数据之前,您必须创建实体并附加它(在您的情况下,必须为每个要持久化的属性设置修改状态,而不是整个实体(您可以想象,开发人员可能会忘记这样做)

第一个解决方案在您的存储库中引入了一种特殊的方法,它可以执行以下操作:

public T Create(long id) {
    T entity = _dbContext.Set<T>().Create();
    entity.Id = id;
    _dbContext.Set<T>().Attach(entity);
    return entity;
}

虽然其他两个答案很好地说明了您可以如何避免这个问题,但我认为值得指出几点

  • 您试图做的事情(即代理实体更新)是极端的EF centeric,IMO实际上在EF上下文之外没有意义,因此一般存储库以这种方式运行是没有意义的
  • 实际上,对于EF,您甚至还没有获得完全正确的流,如果您附加了一个具有几个已设置字段的对象,EF将简化您告诉它的当前DB状态,除非您修改值或设置修改的标志。要在没有选择的情况下执行所尝试的操作,您通常会附加一个没有名称的对象,然后在附加ID对象后设置名称
  • 您的方法通常是出于性能原因而使用的,我建议,通过在现有框架之上进行抽象,您几乎总是会遇到一些逻辑性能下降的问题。如果这是一个大问题,也许你不应该使用存储库?为满足性能问题而向存储库中添加的内容越多,存储库就越复杂,限制也越大,提供多个实现就越困难
尽管如此,我确实认为你可以在一般情况下处理这个特殊情况

这是一种可能的方法

public void UpdateProperty(Expression<Func<T,bool>> selector, FunctionToSetAProperty setter/*not quite sure of the correct syntax off the top of my head*/)
{
   // look in local graph for T and see if you have an already attached version
   // if not attach it with your selector value set
   // set the property of the setter
}
public void UpdateProperty(表达式选择器,functiontosetproperty setter/*我不太清楚正确的语法*/)
{
//在本地图中查找T,看看是否有已附加的版本
//如果没有,请将其与选择器值集一起附加
//设置setter的属性
}
希望这有点道理,我不是在我的开发盒atm机旁,所以我不能真正做一个工作样本


我认为这是一种更好的通用存储库方法,因为它允许您以多种不同的方式实现相同的行为。abovc可能适用于EF,但如果您有内存存储库(例如),则会有不同的方法。此方法允许您实现不同的实现,以满足目的,而不是将存储库限制为仅像EF一样工作。

第一种方法存在一个小问题,将值类型属性设置为其默认值不会作为更改。例如,如果保留登录尝试失败的计数器,并且希望将其重置为0,则该计数器将不起作用。可以在附加之前将其设置为0以外的值(例如1),然后在附加之后将其更改为零。
userRepository(user, u => u.Name);
public void UpdateProperty(Expression<Func<T,bool>> selector, FunctionToSetAProperty setter/*not quite sure of the correct syntax off the top of my head*/)
{
   // look in local graph for T and see if you have an already attached version
   // if not attach it with your selector value set
   // set the property of the setter
}