翻译水平有限,谬误之处还请各位大神指正!
简介:
DBContext.SaveChanges()方法返回写在底层数据库的状态入口的数量,如果你的代码表现异常或者抛出DbUpdateException,这个方法并不能提供太多帮助。在监视窗口里面,你可以轻松地看到DbSet.Local的内容,但是在断点里面搜寻DbChangeTracker.Entries()里面的实体的状态却非常困难。
// (added, modified, deleted)
using StateTuple = System.Tuple<int?, int?, int?>;
using DetailsTuple = System.Tuple<IList, IList, IList>;
现有的DiagnosticsContext可以在断点调试中展示字典(Dictionary)属性。除了调试和记录日志,你可以在漫长的更新过程中为用户展示更多信息。字典的Key属性是实体类型,value属性是Int?或者IList元素类型构成的三元组(新增状态,修改状态,删除状态)。如果某个元素的状态没有发生改变,则元素为Null,如果实体有任何更改,字典入口是唯一呈现。
public enum DiagnosticsContextMode { None, Current, Total, CurrentDetails, TotalDetails}
public abstract class DiagnosticsContext : DbContext
{
protected DiagnosticsContext(string nameOrConnectionString)
: base(nameOrConnectionString) {}
// Default is DiagnosticsContextMode.None.
public DiagnosticsContextMode EnableDiagnostics { get; set; }
// Optionally prints change information on SaveChanges in debug mode.
public DiagnosticsContextMode AutoDebugPrint { get; set; }
// Has one entry (added, modified, deleted) for every monitored type, that has unsaved changes.
// Available after SaveChanges returns, valid until next call to SaveChanges.
public Dictionary<Type, StateTuple> CurrentChanges { get; private set; }
// Holds accumulated CurrentChanges contents during context lifetime.
// Available after first SaveChanges returns, otherwise null.
public Dictionary<Type, StateTuple> TotalChanges { get; private set; }
// Has one entry (added, modified, deleted) for every monitored type, that has unsaved changes.
// Available after SaveChanges returns, valid until next call to SaveChanges.
public Dictionary<Type, DetailsTuple> CurrentChangeDetails { get; private set; }
// Holds accumulated TotalChangeDetails contents during context lifetime.
// Available after first SaveChanges returns, otherwise null.
public Dictionary<Type, DetailsTuple> TotalChangeDetails { get; private set; }
}
CurrentChanges和CurrentChangesDetails在每次执行SaveChangesAsync的时候都会被更新,TotalChanges和TotalChangeDetails在其上下文生命周期期间积累这些更改。注意到,当SaveChangesAsync方法失败的时候,CurrentChanges[Details]仍然是有效的。即使关闭了调试,DiagnosticsContext也能成为你的救星(参见 >>一些亮点)。
背景:
private IEnumerable<EntityType> GetEntityTypes()
{
MetadataWorkspace metadata = ((IObjectContextAdapter)this).ObjectContext.MetadataWorkspace;
return metadata.GetItemCollection(DataSpace.OSpace).GetItems<EntityType>();
}
第一次调用SaveChanges()的时候,一旦所有衍生的上下文的实体类型都被定义后,将获得DiagnosticsContext。其中包含基类和衍生类,按照EF发现他们的顺序排序。
private IEnumerable<EntityType> GetEntityTypes()
{
MetadataWorkspace metadata = ((IObjectContextAdapter)this).ObjectContext.MetadataWorkspace;
return metadata.GetItemCollection(DataSpace.OSpace).GetItems<EntityType>();
}
EntityType对象将被转换成System.Type的实例,条件是DiagnosticsContext存在于同一程序集,而这个程序集就是你定义实体的程序集。否则,衍生类必须提供该程序集允许的转换方法。
protected virtual IList<Type> GetMonitoredTypes(IEnumerable<EntityType> entityTypes)
{
return entityTypes.Select(x => Type.GetType(x.FullName, true /* throwOnError */)).ToList();
}
为了一处不能被检测的类型,衍生类需要重写GetMonitoredTypes()方法,同时需要对可检测的类型按照字典实体和调试输出的顺序进行合理的重新排序。
调用SaveChanges()的时候,可以获得在DbChangeTracker中的被更改的实体。DiagnosticsContext只会对实体的更改做一次检测,因为每次检测都会额外调用一次DetectChanges()。我们可以从detail collection中获得相关的更改总量。
private IList<DbEntityEntry> getChangeTrackerEntries()
{
return ChangeTracker.Entries()
.Where(x => x.State != EntityState.Unchanged && x.State != EntityState.Detached)
.ToArray();
}
为了使用Linq,我使用了一个用来辅助的泛型类,该类继承自非泛型接口。
private interface IHelper
{
StateTuple GetChange(IList<DbEntityEntry> dbEntityEntries);
DetailsTuple GetChangeDetails(IList<DbEntityEntry> dbEntityEntries);
}
private class Helper<T> : IHelper where T : class {}
非泛型的代码现在可以通过IHelper调用泛型版本。检测的类型每次被改变的时候都会创建一个Helper实例,该实例被储藏在一个静态的字典里面。
private StateTuple getChange(Type type, IList<DbEntityEntry> dbEntityEntries)
{
return getHelper(type).GetChange(dbEntityEntries);
}
private static IHelper getHelper(Type type)
{
constructedHelpers = constructedHelpers ?? new Dictionary<Type, IHelper>();
IHelper helper;
if (constructedHelpers.TryGetValue(type, out helper))
{
return helper;
}
Type helperType = typeof(Helper<>).MakeGenericType(type);
constructedHelpers.Add(type, helper = (IHelper)Activator.CreateInstance(helperType));
return helper;
}
GetChange()针对某一类型的泛型实现:
public StateTuple GetChange(IList<DbEntityEntry> dbEntityEntries)
{
dbEntityEntries = dbEntityEntries
.Where(x => x.Entity is T)
.ToArray();
var countPerState = dbEntityEntries.GroupBy(x => x.State,
(state, entries) => new
{
state,
count = entries.Count()
})
.ToArray();
var added = countPerState.SingleOrDefault(x => x.state == EntityState.Added);
var modified = countPerState.SingleOrDefault(x => x.state == EntityState.Modified);
var deleted = countPerState.SingleOrDefault(x => x.state == EntityState.Deleted);
StateTuple tuple = new StateTuple(
added != null ? added.count : (int?)null,
modified != null ? modified.count : (int?)null,
deleted != null ? deleted.count : (int?)null);
return tuple.Item1 == null && tuple.Item2 == null && tuple.Item3 == null ? null : tuple;
}
最后创建为每一种被检测的类型创建一个实体字典。
private Dictionary<Type, StateTuple> getChanges(IEnumerable<Type> types )
{
IList<DbEntityEntry> dbEntityEntries = getChangeTrackerEntries();
Dictionary<Type, StateTuple> dic = types
.Select(x => new { type = x, tuple = getChange(x, dbEntityEntries) })
.Where(x => x.tuple != null)
.ToDictionary(x => x.type, x => x.tuple);
// empty dic: although ChangeTracker.HasChanges() there were no changes for the specified types
return dic.Count != 0 ? dic : null;
}
获得更改的集合的方法与之类似,这里就不做展示了。
使用代码:
从DiagnosticsContext获得你的实例上下文或者通用基类上下文。只有在需要展示DbSet属性的上下文中,你才需要重写GetMonitoredTypes()。编译、运行,找到GetMonitoredTypes()的Assert。将生成的Add声明复制到GetMonitoredTypes()方法体中,更新Assert声明,这样在将来你的模型发生了更改,你也可以知道。
public class YourContext : DiagnosticsContext
{
public YourContext(string nameOrConnectionString) : base(nameOrConnectionString)
protected override IList<Type> GetMonitoredTypes(IEnumerable<EntityType> entityTypes)
{
IList<Type> allTypes = base.GetMonitoredTypes(entityTypes);
IList<Type> types = new List<Type>();
// prints 'types.Add(allTypes.Single(x => x == typeof(a Type)));'
Debug.Print(string.Join(Environment.NewLine,
allTypes.Select(x => string.Format("types.Add(allTypes.Single
(x => x == typeof({0})));", x.Name))));
Debug.Assert(types.Count == allTypes.Count - 0);
return types;
}
public DbSet<YourEntity> YourEntitySet { get; set; }
...
}
跟EnableDiagnostics属性和AutoDebugPrint属性兜兜转转,你也许会想修改DiagnosticsContext以使用ILogger服务。
DiagnosticsContext提供了一些方法,可以随时调用,而不需要依赖于EnableDiagnostics属性值。但是一定要谨慎,因为它会每次都调用DetectChanges()方法。
public Dictionary<Type, StateTuple> GetCurrentChanges()
public Dictionary<Type, StateTuple> GetCurrentChanges(IEnumerable<Type> types)
public Dictionary<Type, DetailsTuple> GetChangeDetails()
public Dictionary<Type, DetailsTuple> GetChangeDetails(IEnumerable<Type> types)
public Tuple<ICollection<T>, ICollection<T>,
ICollection<T>> GetChangeDetails<T>() where T : class
public void IgnoreNextChangeState(EntityState state, params Type[] ignoredTypes)
通常情况下,我们需要添加一些实体,保存他们以获得key值,然后添加关联的导航实体并保存。第二次保存时,将会把之前添加的实体显示为被正确修改。为了避免这情况,我们在第二次保存之前调用IgnoreNextChangeState(EntityState.Modified, types)即可。
一些亮点:
SaveChanges() throws and your context is running with DiagnosticsContextMode.None: all dictionary properties are null, bummer!
QuickWatch and context.GetChangeDetails() will rescue you.