(翻译)Entity Framework技巧系列之十三 - Tip 51 - 55
提示51. 怎样由任意形式的流中加载EF元数据
在提示45中我展示了怎样在运行时生成一个连接字符串,这相当漂亮。
其问题在于它依赖于元数据文件(.csdl .ssdl .msl)存在于本地磁盘上。
但是如果这些文件存在于web服务器中或者类似的位置,甚至你无权访本地文件系统而无法把它们拷贝到本地呢?
原来你也可以由流中加载元数据,这篇提示将告诉你怎么做。
步骤1:获得用于CSDL,MSL与SSDL的XmlTextReaders:
这可以尽可能的简单,如‘new XmlTextReader(url)’。
但是在这个例子中我准备展示给你怎样由字符串变量来完成这个操作,当然这个字符串你可以由任意地方得到:
1 string csdl = "…"; 2 string ssdl = "…"; 3 string msl = "…"; 4 var csdlReader = new StringReader(csdl); 5 var ssdlReader = new StringReader(ssdl); 6 var mslReader = new StringReader(msl); 7 var csdlXmlReader = new XmlTextReader(csdlReader); 8 var ssdlXmlReader = new XmlTextReader(ssdlReader); 9 var mslXmlReader = new XmlTextReader(mslReader);
步骤2:创建元数据ItemCollections:
接下来你需要为CSDL准备一个EdmItemCollection,为SSDL准备一个StoreItemCollection以及为MSL准备一个StorageMappingItemCollection:
1 var edmItemCollection = new EdmItemCollection( 2 new[] { csdlXmlReader } 3 ); 4 var storeItemCollection = new StoreItemCollection( 5 new[] { ssdlXmlReader } 6 ); 7 var storeMappingItemCollection = 8 new StorageMappingItemCollection( 9 edmItemCollection, 10 storeItemCollection, 11 new [] { mslXmlReader } 12 );
远程还感兴趣的唯一对象就是StorageMappingItemCollection,与MSL一起,需要其它ItemCollection来验证映射。
步骤3:创建一个MetadataWorkspace:
接下来你需要将这些ItemCollection组合到一个MetadataWorkspace:
1 var mdw = new MetadataWorkspace(); 2 mdw.RegisterItemCollection(edmItemCollection); 3 mdw.RegisterItemCollection(storeItemCollection); 4 mdw.RegisterItemCollection(storeMappingItemCollection);
步骤4:创建一个EntityConnection:
最后我们需要一个EntityConnection。要创建一个EntityConnection我们需要一个本地数据库连接 – 一般情况下这是一个SqlConnection,但是因为EF有一个提供程序模型,这个连接也可以为其它如Oracle数据库的连接字符串:
1 var sqlConnectionString = @"Data Source=.\SQLEXPRESS;Ini…"; 2 var sqlConnection = new SqlConnection(sqlConnectionString); 3 var entityConnection = new EntityConnection( 4 metadataWorkspace, 5 sqlConnection 6 );
步骤5:创建ObjectContext并按需求使用
最后一步使用你刚创建的EntityConnection来构造你的ObjectContext,并像一般情况一样使用:
1 using (var ctx = new ProductCategoryEntities(entityConnection)) 2 { 3 foreach (var product in ctx.Products) 4 Console.WriteLine(product.Name); 5 }
就是这样…不一定简单,一旦你了解怎样做了也算简单。
提示52. 怎样在数据服务客户端中重用类型
默认情况下,当你添加一个数据服务的引用后你会得到自动生成的代码,其中包含一个强类型的DataServiceContext及所有ResourceType的类。
点击“显示所有文件”可以看到项目中这个自动生成的代码:
然后展开你的数据服务的引用,再展开从属的Reference.datasvcmap,然后打开Reference.cs文件:
步骤1 – 关闭代码生成
如果你要重用一些已存在的类,你需要关闭代码生成。
这相当容易 – 选择Reference.datasvcmap文件进入它的属性,然后清空Custom Tool属性,其默认值为‘DataServiceClientGenerator’。
步骤2 – 创建一个强类型的DataServiceContext:
当我们关闭了代码生成,我们也失去了使编程更方便的强类型的DataServiceContext。
你可以像这样编写自己的DataServiceContext,代码相当容易:
1 public class SampleServiceCtx: DataServiceContext 2 { 3 public SampleServiceCtx(Uri serviceRoot) : base(serviceRoot) { 4 base.ResolveName = ResolveNameFromType; 5 base.ResolveType = ResolveTypeFromName; 6 } 7 protected Type ResolveTypeFromName(string typeName) 8 { 9 if (typeName.StartsWith("Sample.")) 10 { 11 return this.GetType().Assembly.GetType( 12 typeName.Replace("Sample.", "Tip52.") 13 false); 14 } 15 return null; 16 } 17 protected string ResolveNameFromType(Type clientType) 18 { 19 if (clientType.Namespace.Equals("Tip52")) 20 { 21 return "Sample." + clientType.Name; 22 } 23 return null; 24 } 25 public DataServiceQuery<Product> Products { 26 get { 27 return base.CreateQuery<Product>("Products"); 28 } 29 } 30 }
注意Product属性简单返回DataServiceQuery<Product>,其中Product是我们试图重用的类型。
使这工作的关键是由数据服务Resource typeName映射到一个客户端Type的代码,反之亦如此。
这个映射由两个函数来控制,我们在构造函数中告诉DataServiceContext这两个函数。你可以看到在这个例子中我们只是简单的由客户端的’Tip52’的命名空间到服务器端’Sample’这个命名空间。
步骤3 – 试一试:
一旦你建立了解析器,你就可以很容易的重用已存在的类型:
1 var root = new Uri("http://localhost/Tip52/sample.svc"); 2 var ctx = new SampleServiceCtx(root); 3 foreach (Product p in ctx.Products) 4 { 5 Console.WriteLine("{0} costs {1}", p.Name, p.Price); 6 p.Price += 0.30M; // Cross the board price increases! 7 ctx.UpdateObject(p); 8 } 9 ctx.SaveChanges();
就是这样,哈。
警告:
仅当客户端与服务器两端属性名称相同时这才可以工作,因为没有方法重命名属性。
同样由于数据服务客户端中对象实体化工作的方式,当你的类有一个Reference属性及一个backing外键属性并且类进行了自动fix-up使两个值保持一致时这将不会工作。
提示53. 怎样调试EF POCO映射的问题
如果你尝试在EF4.0中使用POCO类,相对更容易在将模型映射到CLR类时遇到问题。
如果你在这里遇到任何恼人的问题,弄清楚事情的最佳的方法是尝试显式为POCO类型加载元数据。
例如,假如Product是一个POCO类型,并且你在使其工作时遇到问题,你可以尝试下这段代码来找出问题是什么:
1 using (MyContext ctx = new MyContext()) 2 { 3 ctx.MetadataWorkspace.LoadFromAssembly( 4 typeof(Product).Assembly, 5 MappingWarning 6 ); 7 }
其中MappingWarning可能是任意接受一个字符串且无返回值的方法,如这样:
1 public void MappingWarning(string warning) 2 { 3 Console.WriteLine(warning); 4 }
当你完成这步,EF将遍历你程序集中的类型,试图查看它们是否与概念模型中的类型匹配,如果某些原因一个CLR类型被识别并且随后被排除-不是一个有效的匹配-你的方法将被调用,接着在我们例子中警告会弹出到控制台。
提示54. 怎样使用声明表达式(Statement Expression)提升性能
背景:
在最近编写数据服务提供程序系列博文的过程中,我停止编写这种使用反射将属性值由一个对象拷贝到另一个的代码段:
1 foreach (var prop in resourceType 2 .Properties 3 .Where(p => (p.Kind & ResourcePropertyKind.Key) 4 != ResourcePropertyKind.Key)) 5 { 6 var clrProp = clrType 7 .GetProperties() 8 .Single(p => p.Name == prop.Name); 9 var defaultPropValue = clrProp 10 .GetGetMethod() 11 .Invoke(resetTemplate, new object[] { }); 12 clrProp 13 .GetSetMethod() 14 .Invoke(resource, new object[] { defaultPropValue }); 15 }
问题:
这段代码至少有两个主要问题。
- 其通过反射在一个循环中查找属性与方法
- 在一个循环中使用反射调用那些方法。
我们可以将所有属性的get/set方法存储于一些可具有缓存能力的数据结构来解决问题(1)。
但是修复问题(2)需要多一点技巧,要使这段代码真正泛型化,你需要一些如轻量级代码生成之类的东西。
解决方案:
多亏.NET4.0添加了声明表达式。这意味着现在你可以创建描述多行声明的表达式,并且那些语句可以像其他任务一样执行。
波拉特说‘好’…
在网上一番搜索,找到了Bart的这篇关于声明表达式的优秀博文,这足以让我激动万分,希望对你也是如此。
步骤1 – 熟悉API
带着发现新东西的热情,我决定先找些简单的东西尝试,尝试把这个委托对象转换为一个表达式:
1 Func<int> func = () => { 2 int n; 3 n=2; 4 return n; 5 };
如果你只这样做,结果很可怕:
1 Expression<Func<int>> expr = () => { 2 int n; 3 n = 2; 4 return n; 5 };
但不幸的是当前C#不支持这种写法,取而代之的是你需要手工构建这个表达式,像这样:
1 var n = Expression.Variable(typeof(int)); 2 var expr = Expression.Lambda<Func<int>> 3 ( 4 Expression.Block( 5 // int n; 6 new[] { n }, 7 // n = 2; 8 Expression.Assign( 9 n, 10 Expression.Constant(2) 11 ), 12 // return n; 13 n 14 ) 15 );
相当容易哈。
步骤2 – 证明我们可以指定到一个属性
现在我们已经体验了下这个API,到时间用它来解决我们的问题了。
我们需要的是一个函数,其通过重设一个对象的一个或多个属性来修改一个对象(本例中为product),像这样:
1 Action<Product> baseReset = (Product p) => { p.Name = null; };
这段代码使用新的表达式API来创建一个等价的操作(action):
1 var parameter = Expression.Parameter(typeof(Product)); 2 var resetExpr = Expression.Lambda<Action<Product>>( 3 Expression.Block( 4 Expression.Assign( 5 Expression.Property(parameter,"Name"), 6 Expression.Constant(null, typeof(string)) 7 ) 8 ), 9 parameter 10 ); 11 var reset = resetExpr.Compile(); 12 var product = new Product { ID = 1, Name = "Foo" }; 13 reset(product);
果然当重置(product)被调用后,product的Name变为null。
步骤3 – 设定到所有的非键属性
现在我们需要做的只是创建一个函数,其接收到一个特定的CLR类型时将创建一个表达式来重置所有的非键属性。
实际上收集属性与它们期望的值的列表,对于本话题不是很重要,所以让我们想象我们已经将获得的信息放到一个dictionary中,如这样:
1 var properties = new Dictionary<PropertyInfo, object>(); 2 var productProperties = typeof(Product).GetProperties(); 3 var nameProp = productProperties.Single(p => p.Name == "Name"); 4 var costProp = productProperties.Single(p => p.Name == "Cost"); 5 properties.Add(nameProp, null); 6 properties.Add(costProp, 0.0M);
有了这个数据结构后,我们的工作就是创建一个与下面的Action有同样效果的表达式:
1 Action<Product> baseReset = (Product p) => { 2 p.Name = null; 3 p.Cost = 0.0M; 4 };
首先我需要创建所有的赋值表达式:
1 var parameter = Expression.Parameter(typeof(Product)); 2 List<Expression> assignments = new List<Expression>(); 3 foreach (var property in properties.Keys) 4 { 5 assignments.Add(Expression.Assign( 6 Expression.Property(parameter, property.Name), 7 Expression.Convert( 8 Expression.Constant( 9 properties[property], 10 property.PropertyType 11 ) 12 ) 13 ); 14 }
接下来我们把赋值表达式注入Lambda内一个block中,编译整个项目并测试我们的新函数:
1 var resetExpr = Expression.Lambda<Action<Product>>( 2 Expression.Block( 3 assignments.ToArray() 4 ), 5 parameter 6 ); 7 var reset = resetExpr.Compile(); 8 var product = new Product { ID = 1, Name = "Foo", Cost = 34.5M }; 9 reset(product); 10 Debug.Assert(product.Name == null); 11 Debug.Assert(product.Cost == 0.0M);
正如期待的这很好用。
要解决这个问题,我准备放在更新的博文中,我们需要一个以类型为键dictionary,我们可以用其来存储一个特定类型重置动作。这样如果一个类型的重置动作没有找到我们只需创建一个新的…
并且性能问题应该已经成为过去
提示55. 怎样通过包装来扩展一个IQueryable
在过去几年中,我在很多情况下都想要深入底层看看IQueryable内部到底发生什么,但我一直没有找到一个简单的方法,至少到目前为止。
像这样深入并做一些东西很有趣,这样你可以:
- 在查询执行前进行Log
- 重写表达式,例如替换一个提供程序-如EF, LINQ to SQL, LINQ to Objects等不支持的表达式为可以被支持的。
无论如何,我很高兴不久之前当查看Vitek(BTW,他的新博客在这)完成的一些示例代码,我意识到我可以将其一般化来创建一个InterceptedQuery<>与一个InterceptingProvider。
基本思想是使你可以这样使用IQueryable:
1 public IQueryable<Customer> Customers 2 { 3 get{ 4 return InterceptingProvider.CreateQuery(_ctx.Customers, visitor); 5 } 6 }
此处visitor是一个会被访问的ExpressionVisitor,在将查询提交到底层(本例中就是Entity Frameork)之前,其可能会重写任何Customers组成的查询。
实现
多亏Vitek才使这个实现变得相当简单。
让我们开始实现InterceptedQuery<>,实际上这不值一提:
1 public class InterceptedQuery<T> : IOrderedQueryable<T> 2 { 3 private Expression _expression; 4 private InterceptingProvider _provider; 5 6 public InterceptedQuery( 7 InterceptingProvider provider, 8 Expression expression) 9 { 10 this._provider = provider; 11 this._expression = expression; 12 } 13 public IEnumerator<T> GetEnumerator() 14 { 15 return this._provider.ExecuteQuery<T>(this._expression); 16 } 17 IEnumerator IEnumerable.GetEnumerator() 18 { 19 return this._provider.ExecuteQuery<T>(this._expression); 20 } 21 public Type ElementType 22 { 23 get { return typeof(T); } 24 } 25 public Expression Expression 26 { 27 get { return this._expression; } 28 } 29 public IQueryProvider Provider 30 { 31 get { return this._provider; } 32 } 33 }
接下来是InterceptedProvider,这更有趣:
1 public class InterceptingProvider : IQueryProvider 2 { 3 private IQueryProvider _underlyingProvider; 4 private Func<Expression,Expression>[] _visitors; 5 6 private InterceptingProvider( 7 IQueryProvider underlyingQueryProvider, 8 params Func<Expression,Expression>[] visitors) 9 { 10 this._underlyingProvider = underlyingQueryProvider; 11 this._visitors = visitors; 12 } 13 14 public static IQueryable<T> Intercept<T>( 15 IQueryable<T> underlyingQuery, 16 params ExpressionVisitor[] visitors) 17 { 18 Func<Expression, Expression>[] visitFuncs = 19 visitors 20 .Select(v => (Func<Expression, Expression>) v.Visit) 21 .ToArray(); 22 return Intercept<T>(underlyingQuery, visitFuncs); 23 } 24 25 public static IQueryable<T> Intercept<T>( 26 IQueryable<T> underlyingQuery, 27 params Func<Expression,Expression>[] visitors) 28 { 29 InterceptingProvider provider = new InterceptingProvider( 30 underlyingQuery.Provider, 31 visitors 32 ); 33 return provider.CreateQuery<T>( 34 underlyingQuery.Expression); 35 } 36 public IEnumerator<TElement> ExecuteQuery<TElement>( 37 Expression expression) 38 { 39 return _underlyingProvider.CreateQuery<TElement>( 40 InterceptExpr(expression) 41 ).GetEnumerator(); 42 } 43 public IQueryable<TElement> CreateQuery<TElement>( 44 Expression expression) 45 { 46 return new InterceptedQuery<TElement>(this, expression); 47 } 48 public IQueryable CreateQuery(Expression expression) 49 { 50 Type et = TypeHelper.FindIEnumerable(expression.Type); 51 Type qt = typeof(InterceptedQuery<>).MakeGenericType(et); 52 object[] args = new object[] { this, expression }; 53 54 ConstructorInfo ci = qt.GetConstructor( 55 BindingFlags.NonPublic | BindingFlags.Instance, 56 null, 57 new Type[] { 58 typeof(InterceptingProvider), 59 typeof(Expression) 60 }, 61 null); 62 63 return (IQueryable)ci.Invoke(args); 64 } 65 public TResult Execute<TResult>(Expression expression) 66 { 67 return this._underlyingProvider.Execute<TResult>( 68 InterceptExpr(expression) 69 ); 70 } 71 public object Execute(Expression expression) 72 { 73 return this._underlyingProvider.Execute( 74 InterceptExpr(expression) 75 ); 76 } 77 private Expression InterceptExpr(Expression expression) 78 { 79 Expression exp = expression; 80 foreach (var visitor in _visitors) 81 exp = visitor(exp); 82 return exp; 83 } 84 }
注意任何时候查询被执行,我们拦截当前表达式,其中依次调用了所有已注册的'visitors',然后在底层提供程序上执行最终的表达式。
实现说明:
在基础架构中我们的实现使用了 Func<Expression,Expression> 而非.NET4.0的System.Linq.Expressions.ExpressionVisitor,主要因为.NET对于社区来的稍显迟了, 所以当前很多visitor没有继承自System.Linq.Expressions.ExpressionVisitor。
你只需要再看一下Matt Warren的极佳的IQToolkit,里面有很多例子。
然而我们想鼓励使用System.Linq.Expressions.ExpressionVisitor,所以对此也有一个方便的重载。
同时记住如果你包装了Entity Framework,并进行了任何重写的比较复杂的查询,你需要避免任何调用表达式 – 见Colin的博文。
IQToolbox比较有用的visitor之一被称作ExpressionWriter及其带的一些小插件-其公开了所有构造函数与基础Visit方法-你可以使用它在Entity Framework查询被执行前将表达式输出到控制台:
1 CustomersContext _ctx = new CustomersContext(); 2 ExpressionWriter _writer = new ExpressionWriter(Console.Out); 3 public IQueryable<Customer> Customers{ 4 get{ 5 return InterceptingProvider.Intercept(_ctx.Customers, _writer.Visit); 6 } 7 }
同样你需要留心我们在CreateQuery这个非强类型的方法中使用IQToolbox中有用的TypeHelper类,它确实帮助我们创建正确的泛型InterceptedQuery<>类型的示例。
再次谢谢Matt!
把这些合在一起:
展示最后一个例子。
这里我模拟WCF/ADO.NET数据服务的工作来处理这个请求:
GET ~/People/?$filter=Surname eq 'James'
如果后端是一个非强类型的数据服务提供程序。
1 // Create some data 2 List<Dictionary<string, object>> data = new List<Dictionary<string, object>>(); 3 data.Add( 4 new Dictionary<string, object>{{"Surname", "James"}, {"Firstname", "Alex"}} 5 ); 6 data.Add( 7 new Dictionary<string, object>{{"Surname", "Guard"}, {"Firstname", "Damien"}} 8 ); 9 data.Add( 10 new Dictionary<string, object>{{"Surname", "Meek"}, {"Firstname", "Colin"}} 11 ); 12 data.Add( 13 new Dictionary<string, object>{{"Surname", "Karas"}, {"Firstname", "Vitek"}} 14 ); 15 data.Add( 16 new Dictionary<string, object>{{"Surname", "Warren"}, {"Firstname", "Matt"}} 17 ); 18 // Create a couple of visitors 19 var writer = new ExpressionWriter(Console.Out); 20 var dspVisitor = new DSPExpressionVisitor(); 21 22 // Intercept queries to the L2O IQueryable 23 var queryRoot = InterceptingProvider.Intercept( 24 data.AsQueryable(), // L2O’s iqueryable 25 writer.Visit, // What does the expression look like first? 26 dspVisitor.Visit, // Replace GetValue(). 27 writer.Visit // What does the expression look like now? 28 ); 29 30 // Create a Data Services handle for the Surname property 31 ResourceProperty surname = new ResourceProperty( 32 "Surname", 33 ResourcePropertyKind.Primitive, 34 ResourceType.GetPrimitiveResourceType(typeof(string)) 35 ); 36 37 // Create a query without knowing how to access the Surname 38 // from x. 39 var query = 40 from x in queryRoot 41 where ((string) DataServiceProviderMethods.GetValue(x, surname)) 42 == "James" 43 select x; 44 45 // Execute the query and print some results 46 foreach (var x in query) 47 Console.WriteLine("Found Match:{0}", 48 x["Firstname"].ToString() 49 );
如所见在一个字典列表中我们有一些People数据,我们试图找到姓氏为'James'的那个人。
问题是数据服务不知道怎样有一个字典中得到姓氏。所以它注入一个请求到 DataServiceProviderMethods.GetValue(..) 。
好。
不幸的是这时LINQ to Objects查询提供程序没有足够的上下文信息来处理这个查询 – 像我们在这个查询中这样盲目的调用GetValue会失败。
所以我们拦截这个查询,DSPExpressionVisitor(此处我将不深入)简单的将
DataServiceProviderMethods.GetValue(x, surname)
替换为这样:
x[surname.Name]
如果你查找形式,你可以看到这与下面的是相同的:
x["Surname"]
所以当整个表达式被访问时,你最终会得到如下这样的查询:
1 var query = 2 from x in queryRoot 3 where ((string) x[surname.Name]) == "James" 4 select x;
对于这个Linq to Objects可以很好的处理!
摘要
这是一个通用目的的解决方案允许将一个IQueryable叠加在另一个上,并在查询表达式被传入底层提供程序前翻译/重写/记录它。
Enjoy。