我的WCF4 Rest Service及Entity Framework with POCO之旅(四)——定制Entity
本文将focus几个结合使用WCF REST和Entity Framework with POCO的常见问题。
Entity Type和Property名称的大小写
按照RESTful的习惯,XML或者JSON格式的数据的node名称开头字母一般使用小写,比如,下面是一段Google Buzz API的RESTful返回信息:
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0"> <id>tag:google.com,2010:buzz:z12puk22ajfyzsz</id> <author> <name>Ted Taco</name> <uri>http://www.google.com/profiles/ted</uri> </author> <published>2010-04-23T11:03:34.342Z</published> <updated>2010-04-23T11:03:34.342Z</updated> <activity:object> <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type> <content type="html">Hey, this is my first Buzz Post!</content> <activity:object> <link rel="alternate" type="text/html" href="http://www.google.com/buzz/ted/Hey-this-is-my-first-Buzz-Post" /> <!-- more data --> </entry>
要让我们的Service返回数据node也使用小写字母开头,我们需要给Entity加上[DataContract]和[DataMember] attributes。(当然也可以修改WCF的behavior让它使用我们提供的的serializer但是现在框架既然还能满足需求,我还不想那么做。)
手动加attribute显然不是一个好主意,而且可以想见一旦模型代码重新生成,我们的修改就都没了。我们需要修改生成Entity的T4模板(Model.tt和Model.Context.tt)。
T4模板和其他的一些代码生成器的模板很相似,和现在的ASP.NET也有点相似。主要特点就是在模板内容(对于模板来说类似于纯文本)中嵌入C#代码。
由于T4模板的可读性不是很好,而VS原生又不支持T4的高亮和intellisense,开始编辑之前,最好装一个支持T4高亮的插件。我用的是Visual T4,虽然反应比较慢还有一些bug,不过是免费的,基本也够用了。
我们先看一下现在的Model.tt的全貌:
<#@ template language="C#" debug="false" hostspecific="true"#> <#@ include file="EF.Utility.CS.ttinclude"#><#@ output extension=".cs"#><# CodeGenerationTools code = new CodeGenerationTools(this); MetadataLoader loader = new MetadataLoader(this); CodeRegion region = new CodeRegion(this, 1); MetadataTools ef = new MetadataTools(this); string inputFile = @"WcfRestServiceDemo.edmx"; EdmItemCollection ItemCollection = loader.CreateEdmItemCollection(inputFile); string namespaceName = code.VsNamespaceSuggestion(); EntityFrameworkTemplateFileManager fileManager = EntityFrameworkTemplateFileManager.Create(this); // Write out support code to primary template output file WriteHeader(fileManager); BeginNamespace(namespaceName, code); WriteCustomObservableCollection(); EndNamespace(namespaceName); // Emit Entity Types foreach (EntityType entity in ItemCollection.GetItems<EntityType>().OrderBy(e => e.Name)) { fileManager.StartNewFile(entity.Name + ".cs"); BeginNamespace(namespaceName, code); bool entityHasNullableFKs = entity.NavigationProperties.Any(np => np.GetDependentProperties().Any(p=>ef.IsNullable(p))); #> <#=Accessibility.ForType(entity)#> <#=code.SpaceAfter(code.AbstractOption(entity))#>partial class <#=code.Escape(entity)#><#=code.StringBefore(" : ", code.Escape(entity.BaseType))#> { <# region.Begin("Primitive Properties"); foreach (EdmProperty edmProperty in entity.Properties.Where(p => p.TypeUsage.EdmType is PrimitiveType && p.DeclaringType == entity)) { bool isForeignKey = entity.NavigationProperties.Any(np=>np.GetDependentProperties().Contains(edmProperty)); bool isDefaultValueDefinedInModel = (edmProperty.DefaultValue != null); bool generateAutomaticProperty = false; #> <#=PropertyVirtualModifier(Accessibility.ForProperty(edmProperty))#> <#=code.Escape(edmProperty.TypeUsage)#> <#=code.Escape(edmProperty)#> { <# if (isForeignKey) { #> <#=code.SpaceAfter(Accessibility.ForGetter(edmProperty))#>get { return <#=code.FieldName(edmProperty)#>; } <#=code.SpaceAfter(Accessibility.ForSetter(edmProperty))#>set { <# if (entityHasNullableFKs) { #> try { _settingFK = true; <# PushIndent(CodeRegion.GetIndent(1)); } if (((PrimitiveType)edmProperty.TypeUsage.EdmType).PrimitiveTypeKind == PrimitiveTypeKind.Binary) { #> if (!StructuralComparisons.StructuralEqualityComparer.Equals(<#=code.FieldName(edmProperty)#>, value)) <# } else { #> if (<#=code.FieldName(edmProperty)#> != value) <# } #> { <# foreach (var np in entity.NavigationProperties.Where(np=>np.GetDependentProperties().Contains(edmProperty))) { EdmProperty principalProperty = ef.GetCorrespondingPrincipalProperty(np, edmProperty); if (((PrimitiveType)principalProperty.TypeUsage.EdmType).PrimitiveTypeKind == PrimitiveTypeKind.Binary) { #> if ((<#=code.Escape(np)#> != null) && !StructuralComparisons.StructuralEqualityComparer.Equals(<#=code.Escape(np)#>.<#=code.Escape(principalProperty)#>, value)) <# } else { #> if (<#=code.Escape(np)#> != null && <#=code.Escape(np)#>.<#=code.Escape(principalProperty)#> != value) <# } #> { <# if (!(np.GetDependentProperties().Where(p=>ef.IsNullable(p)).Any() && np.GetDependentProperties().Count() > 1)) { #> <#=code.Escape(np)#> = null; <# } else { #> var previousValue = <#=code.FieldName(np)#>; <#=code.FieldName(np)#> = null; Fixup<#=np.Name#>(previousValue, skipKeys: true); <# } #> } <# } #> <#=code.FieldName(edmProperty)#> = value; } <# if (entityHasNullableFKs) { PopIndent(); #> } finally { _settingFK = false; } <# } #> } <# } else if (isDefaultValueDefinedInModel) { #> <#=code.SpaceAfter(Accessibility.ForGetter(edmProperty))#>get { return <#=code.FieldName(edmProperty)#>; } <#=code.SpaceAfter(Accessibility.ForSetter(edmProperty))#>set { <#=code.FieldName(edmProperty)#> = value; } <# } else { generateAutomaticProperty = true; #> <#=code.SpaceAfter(Accessibility.ForGetter(edmProperty))#>get; <#=code.SpaceAfter(Accessibility.ForSetter(edmProperty))#>set; <# } #> } <# if (!generateAutomaticProperty) { #> private <#=code.Escape(edmProperty.TypeUsage)#> <#=code.FieldName(edmProperty)#><#=code.StringBefore(" = ", code.CreateLiteral(edmProperty.DefaultValue))#>; <# } } region.End(); region.Begin("Complex Properties"); foreach(EdmProperty edmProperty in entity.Properties.Where(p => p.TypeUsage.EdmType is ComplexType && p.DeclaringType == entity)) { #> <#=PropertyVirtualModifier(Accessibility.ForProperty(edmProperty))#> <#=code.Escape(edmProperty.TypeUsage)#> <#=code.Escape(edmProperty)#> { <#=code.SpaceAfter(Accessibility.ForGetter(edmProperty))#>get { return <#=code.FieldName(edmProperty)#>; } <#=code.SpaceAfter(Accessibility.ForSetter(edmProperty))#>set { <#=code.FieldName(edmProperty)#> = value; } } private <#=code.Escape(edmProperty.TypeUsage)#> <#=code.FieldName(edmProperty)#> = new <#=code.Escape(edmProperty.TypeUsage)#>(); <# } region.End(); //////// //////// Write Navigation properties ------------------------------------------------------------------------------------------- //////// region.Begin("Navigation Properties"); foreach (NavigationProperty navProperty in entity.NavigationProperties.Where(np => np.DeclaringType == entity)) { NavigationProperty inverse = ef.Inverse(navProperty); if (inverse != null && !IsReadWriteAccessibleProperty(inverse)) { inverse = null; } #> <# if (navProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many) { #> <#=PropertyVirtualModifier(Accessibility.ForReadOnlyProperty(navProperty))#> ICollection<<#=code.Escape(navProperty.ToEndMember.GetEntityType())#>> <#=code.Escape(navProperty)#> { get { if (<#=code.FieldName(navProperty)#> == null) { <# if (inverse != null || ((AssociationType)navProperty.RelationshipType).IsForeignKey) { #> var newCollection = new FixupCollection<<#=code.Escape(navProperty.ToEndMember.GetEntityType())#>>(); newCollection.CollectionChanged += Fixup<#=navProperty.Name#>; <#=code.FieldName(navProperty)#> = newCollection; <# } else { #> <#=code.FieldName(navProperty)#> = new FixupCollection<<#=code.Escape(navProperty.ToEndMember.GetEntityType())#>>(); <# } #> } return <#=code.FieldName(navProperty)#>; } set { <# if (inverse != null || ((AssociationType)navProperty.RelationshipType).IsForeignKey) { #> if (!ReferenceEquals(<#=code.FieldName(navProperty)#>, value)) { var previousValue = <#=code.FieldName(navProperty)#> as FixupCollection<<#=code.Escape(navProperty.ToEndMember.GetEntityType())#>>; if (previousValue != null) { previousValue.CollectionChanged -= Fixup<#=navProperty.Name#>; } <#=code.FieldName(navProperty)#> = value; var newValue = value as FixupCollection<<#=code.Escape(navProperty.ToEndMember.GetEntityType())#>>; if (newValue != null) { newValue.CollectionChanged += Fixup<#=navProperty.Name#>; } } <# } else { #> <#=code.FieldName(navProperty)#> = value; <# } #> } } private ICollection<<#=code.Escape(navProperty.ToEndMember.GetEntityType())#>> <#=code.FieldName(navProperty)#>; <# } else { #> <#=PropertyVirtualModifier(Accessibility.ForProperty(navProperty))#> <#=code.Escape(navProperty.ToEndMember.GetEntityType())#> <#=code.Escape(navProperty)#> { <# if (inverse != null || ((AssociationType)navProperty.RelationshipType).IsForeignKey) { #> <#=code.SpaceAfter(Accessibility.ForGetter(navProperty))#>get { return <#=code.FieldName(navProperty)#>; } <#=code.SpaceAfter(Accessibility.ForSetter(navProperty))#>set { if (!ReferenceEquals(<#=code.FieldName(navProperty)#>, value)) { var previousValue = <#=code.FieldName(navProperty)#>; <#=code.FieldName(navProperty)#> = value; Fixup<#=navProperty.Name#>(previousValue); } } } private <#=code.Escape(navProperty.ToEndMember.GetEntityType())#> <#=code.FieldName(navProperty)#>; <# } else { #> <#=code.SpaceAfter(Accessibility.ForGetter(navProperty))#>get; <#=code.SpaceAfter(Accessibility.ForSetter(navProperty))#>set; } <# } } } region.End(); region.Begin("Association Fixup"); if (entityHasNullableFKs) { #> private bool _settingFK = false; <# } foreach (NavigationProperty navProperty in entity.NavigationProperties.Where(np => np.DeclaringType == entity)) { NavigationProperty inverse = ef.Inverse(navProperty); if (inverse != null && !IsReadWriteAccessibleProperty(inverse)) { inverse = null; } if ( (inverse != null || ((AssociationType)navProperty.RelationshipType).IsForeignKey) && (navProperty.ToEndMember.RelationshipMultiplicity != RelationshipMultiplicity.Many) ) { var skipKeysArgument = (navProperty.GetDependentProperties().Where(p=>ef.IsNullable(p)).Any() && navProperty.GetDependentProperties().Count() > 1) ? ", bool skipKeys = false" : String.Empty; #> private void Fixup<#=navProperty.Name#>(<#=code.Escape(navProperty.ToEndMember.GetEntityType())#> previousValue<#= skipKeysArgument #>) { <# if (inverse != null) { if (inverse.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many) { #> if (previousValue != null && previousValue.<#=code.Escape(inverse)#>.Contains(this)) { previousValue.<#=code.Escape(inverse)#>.Remove(this); } <# } else { #> if (previousValue != null && ReferenceEquals(previousValue.<#=code.Escape(inverse)#>, this)) { previousValue.<#=code.Escape(inverse)#> = null; } <# } if (inverse.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many) { #> if (<#=code.Escape(navProperty)#> != null) { if (!<#=code.Escape(navProperty)#>.<#=code.Escape(inverse)#>.Contains(this)) { <#=code.Escape(navProperty)#>.<#=code.Escape(inverse)#>.Add(this); } <# foreach (var dependentProperty in navProperty.GetDependentProperties()) { EdmProperty principalProperty = ef.GetCorrespondingPrincipalProperty(navProperty, dependentProperty); if (((PrimitiveType)principalProperty.TypeUsage.EdmType).PrimitiveTypeKind == PrimitiveTypeKind.Binary) { #> if (!StructuralComparisons.StructuralEqualityComparer.Equals(<#=code.Escape(dependentProperty)#>, <#=code.Escape(navProperty)#>.<#=code.Escape(principalProperty)#>)) <# } else { #> if (<#=code.Escape(dependentProperty)#> != <#=code.Escape(navProperty)#>.<#=code.Escape(principalProperty)#>) <# } #> { <#=code.Escape(dependentProperty)#> = <#=code.Escape(navProperty)#>.<#=code.Escape(principalProperty)#>; } <# } #> } <# if (navProperty.GetDependentProperties().Where(p=>ef.IsNullable(p)).Any()) { if (navProperty.GetDependentProperties().Count() > 1) { #> else if (!_settingFK && !skipKeys) <# } else { #> else if (!_settingFK) <# } #> { <# foreach (var dependentProperty in navProperty.GetDependentProperties().Where(p => ef.IsNullable(p))) { #> <#=code.Escape(dependentProperty)#> = null; <# } #> } <# } } else { #> if (<#=code.Escape(navProperty)#> != null) { <#=code.Escape(navProperty)#>.<#=code.Escape(inverse)#> = this; <# foreach (var dependentProperty in navProperty.GetDependentProperties()) { EdmProperty principalProperty = ef.GetCorrespondingPrincipalProperty(navProperty, dependentProperty); if (((PrimitiveType)principalProperty.TypeUsage.EdmType).PrimitiveTypeKind == PrimitiveTypeKind.Binary) { #> if (!StructuralComparisons.StructuralEqualityComparer.Equals(<#=code.Escape(dependentProperty)#>, <#=code.Escape(navProperty)#>.<#=code.Escape(principalProperty)#>)) <# } else { #> if (<#=code.Escape(dependentProperty)#> != <#=code.Escape(navProperty)#>.<#=code.Escape(principalProperty)#>) <# } #> { <#=code.Escape(dependentProperty)#> = <#=code.Escape(navProperty)#>.<#=code.Escape(principalProperty)#>; } <# } #> } <# } } else { if (navProperty.GetDependentProperties().Any()) { #> if (<#=code.Escape(navProperty)#> != null) { <# foreach (var dependentProperty in navProperty.GetDependentProperties()) { EdmProperty principalProperty = ef.GetCorrespondingPrincipalProperty(navProperty, dependentProperty); if (((PrimitiveType)principalProperty.TypeUsage.EdmType).PrimitiveTypeKind == PrimitiveTypeKind.Binary) { #> if (!StructuralComparisons.StructuralEqualityComparer.Equals(<#=code.Escape(dependentProperty)#>, <#=code.Escape(navProperty)#>.<#=code.Escape(principalProperty)#>)) <# } else { #> if (<#=code.Escape(dependentProperty)#> != <#=code.Escape(navProperty)#>.<#=code.Escape(principalProperty)#>) <# } #> { <#=code.Escape(dependentProperty)#> = <#=code.Escape(navProperty)#>.<#=code.Escape(principalProperty)#>; } <# } #> } <# if (navProperty.GetDependentProperties().Where(p => ef.IsNullable(p)).Any()) { if (navProperty.GetDependentProperties().Count() > 1) { #> else if (!_settingFK && !skipKeys) <# } else { #> else if (!_settingFK) <# } #> { <# foreach (var dependentProperty in navProperty.GetDependentProperties().Where(p => ef.IsNullable(p))) { #> <#=code.Escape(dependentProperty)#> = null; <# } #> } <# } } else if (((AssociationType)navProperty.RelationshipType).IsForeignKey) { #> if (<#=code.Escape(navProperty)#> != null) { <# foreach (var fromProperty in ef.GetPrincipalProperties(navProperty)) { #> <#=code.Escape(navProperty)#>.<#=code.Escape(ef.GetCorrespondingDependentProperty(navProperty, fromProperty))#> = <#=code.Escape(fromProperty)#>; <# } #> } <# } } #> } <# } } foreach (NavigationProperty navProperty in entity.NavigationProperties.Where(np => np.DeclaringType == entity)) { NavigationProperty inverse = ef.Inverse(navProperty); if (inverse != null && !IsReadWriteAccessibleProperty(inverse)) { inverse = null; } if ( (inverse != null || ((AssociationType)navProperty.RelationshipType).IsForeignKey) && (navProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many) ) { #> private void Fixup<#=navProperty.Name#>(object sender, NotifyCollectionChangedEventArgs e) { if (e.NewItems != null) { foreach (<#=code.Escape(navProperty.ToEndMember.GetEntityType())#> item in e.NewItems) { <# if (inverse != null) { if (inverse.ToEndMember.RelationshipMultiplicity != RelationshipMultiplicity.Many) { #> item.<#=code.Escape(inverse)#> = this; <# } else { #> if (!item.<#=code.Escape(inverse)#>.Contains(this)) { item.<#=code.Escape(inverse)#>.Add(this); } <# } } else if (((AssociationType)navProperty.RelationshipType).IsForeignKey) { foreach (var fromProperty in ef.GetPrincipalProperties(navProperty)) { #> item.<#=code.Escape(ef.GetCorrespondingDependentProperty(navProperty, fromProperty))#> = <#=code.Escape(fromProperty)#>; <# } } #> } } if (e.OldItems != null) { foreach (<#=code.Escape(navProperty.ToEndMember.GetEntityType())#> item in e.OldItems) { <# if (inverse != null) { if (inverse.ToEndMember.RelationshipMultiplicity != RelationshipMultiplicity.Many) { #> if (ReferenceEquals(item.<#=code.Escape(inverse)#>, this)) { item.<#=code.Escape(inverse)#> = null; } <# } else { #> if (item.<#=code.Escape(inverse)#>.Contains(this)) { item.<#=code.Escape(inverse)#>.Remove(this); } <# } } else if (((AssociationType)navProperty.RelationshipType).IsForeignKey) { foreach (var fromProperty in ef.GetPrincipalProperties(navProperty)) { var p = ef.GetCorrespondingDependentProperty(navProperty, fromProperty); if (ef.IsNullable(p.TypeUsage)) { #> item.<#=code.Escape(p)#> = null; <# } } } ;#> } } } <# } } region.End(); #> } <# EndNamespace(namespaceName); } foreach (ComplexType complex in ItemCollection.GetItems<ComplexType>().OrderBy(e => e.Name)) { fileManager.StartNewFile(complex.Name + ".cs"); BeginNamespace(namespaceName, code); #> <#=Accessibility.ForType(complex)#> partial class <#=code.Escape(complex)#> { <# region.Begin("Primitive Properties"); foreach(EdmProperty edmProperty in complex.Properties.Where(p => p.TypeUsage.EdmType is PrimitiveType && p.DeclaringType == complex)) { bool isDefaultValueDefinedInModel = (edmProperty.DefaultValue != null); #> <#=Accessibility.ForProperty(edmProperty)#> <#=code.Escape(edmProperty.TypeUsage)#> <#=code.Escape(edmProperty)#> <# if (isDefaultValueDefinedInModel) { #> { <#=code.SpaceAfter(Accessibility.ForGetter(edmProperty))#>get { return <#=code.FieldName(edmProperty)#>; } <#=code.SpaceAfter(Accessibility.ForSetter(edmProperty))#>set { <#=code.FieldName(edmProperty)#> = value; } } private <#=code.Escape(edmProperty.TypeUsage)#> <#=code.FieldName(edmProperty)#><#=code.StringBefore(" = ", code.CreateLiteral(edmProperty.DefaultValue))#>; <# } else { #> { <#=code.SpaceAfter(Accessibility.ForGetter(edmProperty))#>get; <#=code.SpaceAfter(Accessibility.ForSetter(edmProperty))#>set; } <# } } region.End(); region.Begin("Complex Properties"); foreach(EdmProperty edmProperty in complex.Properties.Where(p => p.TypeUsage.EdmType is ComplexType && p.DeclaringType == complex)) { #> <#=Accessibility.ForProperty(edmProperty)#> <#=code.Escape(edmProperty.TypeUsage)#> <#=code.Escape(edmProperty)#> { <#=code.SpaceAfter(Accessibility.ForGetter(edmProperty))#>get { return <#=code.FieldName(edmProperty)#>; } <#=code.SpaceAfter(Accessibility.ForSetter(edmProperty))#>set { <#=code.FieldName(edmProperty)#> = value; } } private <#=code.Escape(edmProperty.TypeUsage)#> <#=code.FieldName(edmProperty)#> = new <#=code.Escape(edmProperty.TypeUsage)#>(); <# } region.End(); #> } <# EndNamespace(namespaceName); } if (!VerifyTypesAreCaseInsensitiveUnique(ItemCollection)) { return ""; } fileManager.Process(); #> <#+ void WriteHeader(EntityFrameworkTemplateFileManager fileManager, params string[] extraUsings) { fileManager.StartHeader(); #> //------------------------------------------------------------------------------ // <auto-generated> // This code was generated from a template. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------ using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; <#=String.Join(String.Empty, extraUsings.Select(u => "using " + u + ";" + Environment.NewLine).ToArray())#> <#+ fileManager.EndBlock(); } void BeginNamespace(string namespaceName, CodeGenerationTools code) { CodeRegion region = new CodeRegion(this); if (!String.IsNullOrEmpty(namespaceName)) { #> namespace <#=code.EscapeNamespace(namespaceName)#> { <#+ PushIndent(CodeRegion.GetIndent(1)); } } void EndNamespace(string namespaceName) { if (!String.IsNullOrEmpty(namespaceName)) { PopIndent(); #> } <#+ } } bool IsReadWriteAccessibleProperty(EdmMember member) { string setter = Accessibility.ForWriteOnlyProperty(member); string getter = Accessibility.ForReadOnlyProperty(member); return getter != "private" && getter != "protected" && setter != "private" && setter != "protected"; } string PropertyVirtualModifier(string accessibility) { return accessibility + (accessibility != "private" ? " virtual" : ""); } void WriteCustomObservableCollection() { #> // An System.Collections.ObjectModel.ObservableCollection that raises // individual item removal notifications on clear and prevents adding duplicates. public class FixupCollection<T> : ObservableCollection<T> { protected override void ClearItems() { new List<T>(this).ForEach(t => Remove(t)); } protected override void InsertItem(int index, T item) { if (!this.Contains(item)) { base.InsertItem(index, item); } } } <#+ } bool VerifyTypesAreCaseInsensitiveUnique(EdmItemCollection itemCollection) { Dictionary<string, bool> alreadySeen = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase); foreach(StructuralType type in itemCollection.GetItems<StructuralType>()) { if (!(type is EntityType || type is ComplexType)) { continue; } if (alreadySeen.ContainsKey(type.FullName)) { Error(String.Format(CultureInfo.CurrentCulture, "This template does not support types that differ only by case, the types {0} are not supported", type.FullName)); return false; } else { alreadySeen.Add(type.FullName, true); } } return true; } #>
看起来很长?其实主要内容并不复杂。我们所要关心的的Entity生成代码都在一个遍历所有entity的foreach中,对于每个entity,它依次枚举Primitive Properties - entity本身定义的基本类型字段,Complex Properties - 用户定义的复杂类型字段,以及Navigation Properties - entity的关系字段。
首先找到生成每个类定义的代码:
<#=Accessibility.ForType(entity)#> <#=code.SpaceAfter(code.AbstractOption(entity))#>partial class <#=code.Escape(entity)#><#=code.StringBefore(" : ", code.Escape(entity.BaseType))#>
在上面加一行:
[DataContract(Name = "<#=char.ToLowerInvariant(entity.Name[0]) + entity.Name.Substring(1) #>")]
然后再找到生成Primitive Property的代码(其他的类似):
<#=PropertyVirtualModifier(Accessibility.ForProperty(edmProperty))#> <#=code.Escape(edmProperty.TypeUsage)#> <#=code.Escape(edmProperty)#>
在上面加一行:
[DataMember(Name = "<#=char.ToLowerInvariant(edmProperty.Name[0]) + edmProperty.Name.Substring(1) #>")]
由于DataContract在System.Runtime.Serialization命名空间下,我们还需要添加命名空间。在接近Model.tt末尾的地方找到有一堆命名空间定义,在末尾加上:
using System.Runtime.Serialization;
然后Save,这时代码会重新生成,我们再看一下这时的Microblog.cs:
//------------------------------------------------------------------------------ // <auto-generated> // This code was generated from a template. // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------ using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Runtime.Serialization; namespace WcfRestServiceDemo.Data { [DataContract(Name = "microblog")] public partial class Microblog { #region Primitive Properties [DataMember(Name = "id")] public virtual int Id { get; set; } [DataMember(Name = "content")] public virtual string Content { get; set; } [DataMember(Name = "publishTime")] public virtual System.DateTime PublishTime { get; set; } #endregion } }
这时如果测试一下服务,就可以看到不论是XML还是JSON的返回结果中的字段都已经变成小写字母开头了。
需要注意的是,不仅是返回,这时客户端发送请求时,对应字段也必须同样改成小写字母开头。
处理Null值属性
为了引入这个问题,我们将服务稍稍变得复杂一些。假设现在并且每条微博可以关联一张图片,用户只要在写微博同时,输入图片地址即可。那么修改Microblog模型,添加Picture属性,包含一个图片的URL。由于图片是可选的,所以还要将的Nullable设为true。
如下:
由于修改了EntityModel,需要在两个.tt模板文件上右键选择Run Custom Tools来生成新的POCO entity.
然后我们看一下服务的返回:
<ArrayOfmicroblog xmlns="http://schemas.datacontract.org/2004/07/WcfRestServiceDemo.Data" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"> <microblog> <content>Microblog with no picture.</content> <id>1</id> <picture i:nil="true"/> <publishTime>2011-04-19T22:00:00</publishTime> </microblog> <microblog> <content>Microblog with picture</content> <id>2</id> <picture>http://somepicture/somepicture</picture> <publishTime>2011-04-19T22:00:00</publishTime> </microblog> </ArrayOfmicroblog>
这里有两条microblog,一条有picture,一条没有。但是没有picture的那个也把picture字段给返回了,只是用i:nil=”true”来标识这是个null值。显然这不是理想方式,想象一下一个entity可能会有很多可空的属性,尤其是当一个entity与许多其他entity产生关联,引入了许多navigation properties时,传输这些空属性将严重影响可读性(虽然大多数情况下应该不是人来读)并对性能造成影响(传输、序列化和反序列化)。(当然这不是绝对的,nil值有其应用场景,请根据实际情况决定)
幸好DataMember attribute还有一个EmitDefaultValue属性,可以指定是否要序列化默认值。用和刚才类似的方法,修改T4模板,修改Property定义上方的[DataMember]:
[DataMember(Name = "<#=char.ToLowerInvariant(edmProperty.Name[0]) + edmProperty.Name.Substring(1) #>"<# if(edmProperty.Nullable) { #>, EmitDefaultValue = false<# } #>)]首先判断了property是否可设为null,若可以才省略空值。否则对于一个int属性,值为0时它就不会被传输了,这一般是不太合适的。(确切的讲,这里的Nullable和数据类型没有关系,指的是在数据模型中的nullable,也就是是否为可空字段。)
再次访问服务:
<ArrayOfmicroblog xmlns="http://schemas.datacontract.org/2004/07/WcfRestServiceDemo.Data" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"> <microblog> <content>Microblog with no picture.</content> <id>1</id> <publishTime>2011-04-19T22:00:00</publishTime> </microblog> <microblog> <content>Microblog with picture</content> <id>2</id> <picture>http://somepicture/somepicture</picture> <publishTime>2011-04-19T22:00:00</publishTime> </microblog> </ArrayOfmicroblog>
可以看到没有图片的那个microblog的picture属性已经被省略了。
JSON中的DateTime
在第二章末尾我们提到过DateTime属性在JSON表示下的奇怪格式。严格来说,这不算什么问题,因为既然微软选择了这种格式,那么全世界的人都在碰到这个问题,相应的客户端解决方案也有很多。比如:
- http://archive.cnblogs.com/a/1444633/
- http://stackoverflow.com/questions/206384/how-to-format-json-date
如果一定要从服务端解决这个问题的话,最根本的方法是改用自定义的序列化器。而比较简单的方法是为entity写一个partial类,然后提供一个long格式的对应属性:
using System; using System.Runtime.Serialization; namespace WcfRestServiceDemo.Data { partial class Microblog { private static readonly DateTime _baseTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime(); [DataMember(Name = "publishTimeSec")] public long PublishTimeSeconds { get { return (long)(PublishTime - _baseTime).TotalMilliseconds; } set { PublishTime = _baseTime.AddMilliseconds(value); } } } }
这时服务的返回就是:
[ { "content":"Microblog with no picture.", "id":1, "publishTime":"\/Date(1303221600000+0800)\/", "publishTimeSec":1303221600000 }, { "content":"Microblog with picture", "id":2, "picture":"http:\/\/somepicture\/somepicture", "publishTime":"\/Date(1303221600000+0800)\/", "publishTimeSec":1303221600000 } ]
当然更绝的做法是把这个替代属性的生成也交给T4模板来做。具体做法请参考我整理好后上传的代码。
小结
本文介绍了:
- 如何修改T4模板控制模型代码的生成,包括修改Type名称、属性名称的起始字母大小写和是否要序列化默认值的选项
- 如何用替代属性解决JSON中DateTime格式的问题。
我的WCF4 REST Service及Entity Framework with POCO之旅系列
- 我的WCF4 REST Service及Entity Framework with POCO之旅(一)——创建一个基本的RESTful Service
- 我的WCF4 REST Service及Entity Framework with POCO之旅(二)——选择请求/返回格式
- 我的WCF4 REST Service及Entity Framework with POCO之旅(三)——用Entity Framework和POCO Template实现数据模型及存储
- 我的WCF4 REST Service及Entity Framework with POCO之旅(四)——定制Entity
- 我的WCF4 REST Service及Entity Framework with POCO之旅(五)——身份验证
posted on 2011-04-20 10:44 Gildor Wang 阅读(2781) 评论(6) 编辑 收藏 举报