Sharing validation code between a WPF client, a WCF Service and Entity Framework 4.1 using data annotations

Recently Entity Framework 4.1 was released. While most people were very enthusiastic about the code first stuff, code first was the part that impressed me the least. This is because in my line of business we deal with stored procedures a lot and it turns out that mapping to stored procedures isn’t supported with code first in the 4.1 release. What did catch my eye is the new DBContext api. As you’ll probably know, the new DBContext class is largely a wrapper around the ObjectContext class with a simplified API. Now you might think as I did: “But if I’m comfortable with the ObjectContext class and I don’t use code first, why in the world would I need a DbContext?”. The answer is pretty simple. The DbContext has a feature which simply isn’t supported by the ObjectContext: data validation with the attributes from the System.ComponentModel.DataAnnotations namespace. In this article I’ll show you how to decorate your classes with validation attributes and how these classes can be reused by both the client and the server. The client will be a WPF application. If you don’t like WPF or you just don’t care, feel free the skip to the “WCF Service” paragraph. That’s where the Entity Framework 4.1 part is explained.
The Solution
Let’s first start with the two projects that are responsible for my data access classes, including the entities:
Both DbContextLibrary and EntitiesLibrary are normal class library projects. I started with the DBContextLibrary and used the new “DbContext Generator” T4 template to generate classes. When you use this template you’ll actually get two T4 templates added to your project. The one with “.Context” in it generates your DbContext class, the other one generates POCO entities. I’ve simply moved the one that generates the entities to it’s own class library. If you do this, you’ll have to add a namespace in the T4 template that generates the DbContext, so that DbContext knows the entity classes. In this article I will only have one entity, namely Product. I’ve added some validation to this class using the buddy class mechanism. This code is contained in the Product.Metadata.cs file in the EntitiesLibrary project:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.ComponentModel.DataAnnotations;
6:
7:
8: namespace EntitiesLibrary
9: {
10: [MetadataType(typeof(ProductMetadata))]
11: public partial class Product
12: {
13: private class ProductMetadata
14: {
15: [Range(0,Double.MaxValue,ErrorMessage="ListPrice can't be smaller than zero!")]
16: public decimal ListPrice { get; set; }
17: }
18: }
19: }
You can see that I’ve only added validation for the ListPrice property using the Range attribute. Now let’s take a look at the Adventureworks2008.tt T4 template, since it contains some stuff I had to manually edit:
1: <#@ template language="C#" debug="false" hostspecific="true"#>
2: <#@ include file="EF.Utility.CS.ttinclude"#><#@
3: output extension=".cs"#><#
4:
5: CodeGenerationTools code = new CodeGenerationTools(this);
6: MetadataLoader loader = new MetadataLoader(this);
7: CodeRegion region = new CodeRegion(this, 1);
8: MetadataTools ef = new MetadataTools(this);
9:
10: string inputFile = @"..DbContextLibraryAdventureworks2008.edmx";
11: EdmItemCollection ItemCollection = loader.CreateEdmItemCollection(inputFile);
12: string namespaceName = code.VsNamespaceSuggestion();
13:
14: EntityFrameworkTemplateFileManager fileManager = EntityFrameworkTemplateFileManager.Create(this);
15: WriteHeader(fileManager);
16:
17: foreach (var entity in ItemCollection.GetItems<EntityType>().OrderBy(e => e.Name))
18: {
19: fileManager.StartNewFile(entity.Name + ".cs");
20: BeginNamespace(namespaceName, code);
21: #>
22: using System;
23: using System.Collections.Generic;
24: using System.ComponentModel;
25: using System.ComponentModel.DataAnnotations;
26: using System.Linq;
27: using System.Reflection;
28:
29: <#=Accessibility.ForType(entity)#> <#=code.SpaceAfter(code.AbstractOption(entity))#>partial class <#=code.Escape(entity)#> : IDataErrorInfo
30: {
31: static <#=code.Escape(entity)#>()
32: {
33: //We need to hook this up in order to make the buddy class mechanism work in WPF, Console Applications etc.....
34: //Or the Validator methods won't make use of the metadata class.
35:
36: Type currentType = MethodBase.GetCurrentMethod().DeclaringType;
37: object[] attributes = currentType.GetCustomAttributes(typeof(MetadataTypeAttribute),false);
38: if(attributes.Length > 0)
39: {
40: //MetadataType attribute found!
41: MetadataTypeAttribute metaDataAttribute = (MetadataTypeAttribute)attributes[0];
42: TypeDescriptor.AddProviderTransparent(
43: new AssociatedMetadataTypeTypeDescriptionProvider(
44: currentType, metaDataAttribute.MetadataClassType),currentType);
45: }
46:
47: }
48:
49: public string Error
50: {
51: get
52: {
53: ValidationContext vc = new ValidationContext(this, null, null);
54: List<ValidationResult> result = new List<ValidationResult>();
55: if (!Validator.TryValidateObject(this, vc, result))
56: {
57: return result.First().ErrorMessage;
58: }
59: else
60: {
61: return null;
62: }
63: }
64: }
65:
66: public string this[string columnName]
67: {
68: get
69: {
70: ValidationContext vc = new ValidationContext(this, null, null);
71: List<ValidationResult> result = new List<ValidationResult>();
72: vc.MemberName = columnName;
73: if (!Validator.TryValidateProperty(GetType().GetProperty(columnName).GetValue(this, null), vc, result))
74: {
75: return result.First().ErrorMessage;
76: }
77: else
78: {
79: return null;
80: }
81: }
82: }
83:
84: <#
85: var propertiesWithDefaultValues = entity.Properties.Where(p => p.TypeUsage.EdmType is PrimitiveType && p.DeclaringType == entity && p.DefaultValue != null);
86: var collectionNavigationProperties = entity.NavigationProperties.Where(np => np.DeclaringType == entity && np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many);
87: var complexProperties = entity.Properties.Where(p => p.TypeUsage.EdmType is ComplexType && p.DeclaringType == entity);
88:
89: if (propertiesWithDefaultValues.Any() || collectionNavigationProperties.Any() || complexProperties.Any())
90: {
91: #>
92: public <#=code.Escape(entity)#>()
93: {
94: <#
95: foreach (var edmProperty in propertiesWithDefaultValues)
96: {
97: #>
98: this.<#=code.Escape(edmProperty)#> = <#=code.CreateLiteral(edmProperty.DefaultValue)#>;
99: <#
100: }
101:
102: foreach (var navigationProperty in collectionNavigationProperties)
103: {
104: #>
105: this.<#=code.Escape(navigationProperty)#> = new HashSet<<#=code.Escape(navigationProperty.ToEndMember.GetEntityType())#>>();
106: <#
107: }
108:
109: foreach (var complexProperty in complexProperties)
110: {
111: #>
112: this.<#=code.Escape(complexProperty)#> = new <#=code.Escape(complexProperty.TypeUsage)#>();
113: <#
114: }
115: #>
116: }
117:
118: <#
119: }
120:
121: var primitiveProperties = entity.Properties.Where(p => p.TypeUsage.EdmType is PrimitiveType && p.DeclaringType == entity);
122: if (primitiveProperties.Any())
123: {
124: foreach (var edmProperty in primitiveProperties)
125: {
126: WriteProperty(code, edmProperty);
127: }
128: }
129:
130: if (complexProperties.Any())
131: {
132: #>
133:
134: <#
135: foreach(var complexProperty in complexProperties)
136: {
137: WriteProperty(code, complexProperty);
138: }
139: }
140:
141: var navigationProperties = entity.NavigationProperties.Where(np => np.DeclaringType == entity);
142: if (navigationProperties.Any())
143: {
144: #>
145:
146: <#
147: foreach (var navigationProperty in navigationProperties)
148: {
149: WriteNavigationProperty(code, navigationProperty);
150: }
151: }
152: #>
153: }
154: <#
155: EndNamespace(namespaceName);
156: }
157:
158: foreach (var complex in ItemCollection.GetItems<ComplexType>().OrderBy(e => e.Name))
159: {
160: fileManager.StartNewFile(complex.Name + ".cs");
161: BeginNamespace(namespaceName, code);
162: #>
163: using System;
164:
165: <#=Accessibility.ForType(complex)#> partial class <#=code.Escape(complex)#>
166: {
167: <#
168: var complexProperties = complex.Properties.Where(p => p.TypeUsage.EdmType is ComplexType && p.DeclaringType == complex);
169: var propertiesWithDefaultValues = complex.Properties.Where(p => p.TypeUsage.EdmType is PrimitiveType && p.DeclaringType == complex && p.DefaultValue != null);
170:
171: if (propertiesWithDefaultValues.Any() || complexProperties.Any())
172: {
173: #>
174: public <#=code.Escape(complex)#>()
175: {
176: <#
177: foreach (var edmProperty in propertiesWithDefaultValues)
178: {
179: #>
180: this.<#=code.Escape(edmProperty)#> = <#=code.CreateLiteral(edmProperty.DefaultValue)#>;
181: <#
182: }
183:
184: foreach (var complexProperty in complexProperties)
185: {
186: #>
187: this.<#=code.Escape(complexProperty)#> = new <#=code.Escape(complexProperty.TypeUsage)#>();
188: <#
189: }
190: #>
191: }
192:
193: <#
194: }
195:
196: var primitiveProperties = complex.Properties.Where(p => p.TypeUsage.EdmType is PrimitiveType && p.DeclaringType == complex);
197: if (primitiveProperties.Any())
198: {
199: foreach(var edmProperty in primitiveProperties)
200: {
201: WriteProperty(code, edmProperty);
202: }
203: }
204:
205: if (complexProperties.Any())
206: {
207: #>
208:
209: <#
210: foreach(var edmProperty in complexProperties)
211: {
212: WriteProperty(code, edmProperty);
213: }
214: }
215: #>
216: }
217: <#
218: EndNamespace(namespaceName);
219: }
220:
221: if (!VerifyTypesAreCaseInsensitiveUnique(ItemCollection))
222: {
223: return "";
224: }
225:
226: fileManager.Process();
227:
228: #>
229: <#+
230: string GetResourceString(string resourceName)
231: {
232: if(_resourceManager == null)
233: {
234: _resourceManager = new System.Resources.ResourceManager("System.Data.Entity.Design", typeof(System.Data.Entity.Design.MetadataItemCollectionFactory).Assembly);
235: }
236:
237: return _resourceManager.GetString(resourceName, null);
238: }
239: System.Resources.ResourceManager _resourceManager;
240:
241: void WriteHeader(EntityFrameworkTemplateFileManager fileManager)
242: {
243: fileManager.StartHeader();
244: #>
245: //------------------------------------------------------------------------------
246: // <auto-generated>
247: // <#=GetResourceString("Template_GeneratedCodeCommentLine1")#>
248: //
249: // <#=GetResourceString("Template_GeneratedCodeCommentLine2")#>
250: // <#=GetResourceString("Template_GeneratedCodeCommentLine3")#>
251: // </auto-generated>
252: //------------------------------------------------------------------------------
253:
254: <#+
255: fileManager.EndBlock();
256: }
257:
258: void BeginNamespace(string namespaceName, CodeGenerationTools code)
259: {
260: CodeRegion region = new CodeRegion(this);
261: if (!String.IsNullOrEmpty(namespaceName))
262: {
263: #>
264: namespace <#=code.EscapeNamespace(namespaceName)#>
265: {
266: <#+
267: PushIndent(CodeRegion.GetIndent(1));
268: }
269: }
270:
271:
272: void EndNamespace(string namespaceName)
273: {
274: if (!String.IsNullOrEmpty(namespaceName))
275: {
276: PopIndent();
277: #>
278: }
279: <#+
280: }
281: }
282:
283: void WriteProperty(CodeGenerationTools code, EdmProperty edmProperty)
284: {
285: WriteProperty(Accessibility.ForProperty(edmProperty),
286: code.Escape(edmProperty.TypeUsage),
287: code.Escape(edmProperty),
288: code.SpaceAfter(Accessibility.ForGetter(edmProperty)),
289: code.SpaceAfter(Accessibility.ForSetter(edmProperty)));
290: }
291:
292: void WriteNavigationProperty(CodeGenerationTools code, NavigationProperty navigationProperty)
293: {
294: var endType = code.Escape(navigationProperty.ToEndMember.GetEntityType());
295: WriteProperty(PropertyVirtualModifier(Accessibility.ForProperty(navigationProperty)),
296: navigationProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many ? ("ICollection<" + endType + ">") : endType,
297: code.Escape(navigationProperty),
298: code.SpaceAfter(Accessibility.ForGetter(navigationProperty)),
299: code.SpaceAfter(Accessibility.ForSetter(navigationProperty)));
300: }
301:
302: void WriteProperty(string accessibility, string type, string name, string getterAccessibility, string setterAccessibility)
303: {
304: #>
305: <#=accessibility#> <#=type#> <#=name#> { <#=getterAccessibility#>get; <#=setterAccessibility#>set; }
306: <#+
307: }
308:
309: string PropertyVirtualModifier(string accessibility)
310: {
311: return accessibility + (accessibility != "private" ? " virtual" : "");
312: }
313:
314: bool VerifyTypesAreCaseInsensitiveUnique(EdmItemCollection itemCollection)
315: {
316: var alreadySeen = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
317: foreach(var type in itemCollection.GetItems<StructuralType>())
318: {
319: if (!(type is EntityType || type is ComplexType))
320: {
321: continue;
322: }
323:
324: if (alreadySeen.ContainsKey(type.FullName))
325: {
326: Error(String.Format(CultureInfo.CurrentCulture, "This template does not support types that differ only by case, the types {0} are not supported", type.FullName));
327: return false;
328: }
329: else
330: {
331: alreadySeen.Add(type.FullName, true);
332: }
333: }
334:
335: return true;
336: }
337: #>
In order for entities to support automatic validation in WPF I had to change the following things in this template:
- Line 10: The path to the .edmx file. The .edmx is contained in another project and the T4 template by default assumes it’s in the same project as the T4 template, so you’ll need to adjust this.
- Lines 24-27: I’ve added these namespaces because they are necessary for the the rest of my code.
- Line 29: I’ve changed this line to let my entity implement the IDataErrorInfo interface. You have to understand that in WPF, unlike in Silverlight, the data annotations aren’t automatically read by the components (DataGrid). You’ll have to implement the IDataErrorInfo interface and validate manually using the data annotations.
- Line 49-82: These are the members you’ll have to implement because of the IDataErrorInfo interface: Error and an indexer. Normally, in the Error property you’ll write code to validate the whole object and construct a string with an error message. In the indexer you’ll write code that validates a single property. The property name is supplied as an argument and you’ll need to construct a string with an error message. In both members I’ve used the Validator class to validate. Whenever you are in a situation that you’ll need to validate using data annotations and this doesn’t happen automatically, you can use the static Validator class to do the validation for you. This isn’t difficult, it’s just something to keep in mind. To make the validation generic for property validation, you’ll have to supply the member name like on line 72. Next to that, you’ll have to use some reflection to get the property value in a generic way. This reflection code is found on line 73, it’s the first argument of the TryValidatePropertyMethod.
- Line 31-47: I’ve intentionally covered lines 49-82 first. Because now it’s clear that I implement the IDataErrorInfo interface in my entity and I use the Validator class to do the validation for me. If you now create a list of Products, add those to a WPF datagrid and use TwoWay binding with validation enabled for the ListPrice, you would think that validation should kick in. This was at least what I thought. Turns out that the whole buddy metadata mechanism isn’t something that’s supported by the static Validator class. It are the frameworks like WCF RIA Services that add support for the buddy mechanism. Luckily with some googling and reflectoring I came to the conclusion that the Validator class uses .Net’s extensible TypeDescriptor mechanism instead of reflection, which can be seen as a mechanism similar to reflection, only the type information can be extended runtime. I had to extend the type information once for each entity that get’s generated, with the information in the buddy metadata class. I can’t do this in the constructor, because then it would happen way to often. A static constructor seemed a good place to me. The declaration of the static constructor is found on line 31. On line 36 I use the MethodBase class to get a reference to the current type. This is needed since I need to extend the type information of the current type. On line 37-39 I check whether the MetadataTypeAttribute is present. If so, I retrieve it on line 41. This attribute has the property “MetadataClassType”. This property contains the type you supply when you place the MetadataType attribute. On line 42 I extend the type information of the current type with the associated metadata. I do this by calling the AddProviderTransparent method on the TypeDescriptor class and supplying an AssociatedMetadataTypeTypeDescriptionProvider. This provider is used for the buddy class mechanism and it’s constructor takes two arguments. First is the type you wish to extend with extra metadata, second is the metadata class.
So know we have a T4 template, that generates entities with support for WPF validation using data annotations and the buddy mechanism. Of course you can use the generated entities for any technology that doesn’t use the data annotations by default.
The WPF Application
The article is getting quite long, so bare with me :). Luckily, this paragraph will be short. Here is the XAML:
1: <Window x:Class="ProductsApp.MainWindow"
2: >="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: >="http://schemas.microsoft.com/winfx/2006/xaml"
4: Title="MainWindow" Height="350" Width="525" mc:Ignorable="d" >="http://schemas.microsoft.com/expression/blend/2008" >="http://schemas.openxmlformats.org/markup-compatibility/2006" >="clr-namespace:EntitiesLibrary;assembly=EntitiesLibrary" >
5: <Grid >
6: <DataGrid AutoGenerateColumns="False" EnableRowVirtualization="True" Name="_dtgProducts" RowDetailsVisibilityMode="VisibleWhenSelected">
7: <DataGrid.Columns>
8: <DataGridTextColumn x:Name="classColumn" Binding="{Binding Path=Class}" Header="Class" Width="SizeToHeader" />
9: <DataGridTextColumn x:Name="colorColumn" Binding="{Binding Path=Color}" Header="Color" Width="SizeToHeader" />
10: <DataGridTextColumn x:Name="daysToManufactureColumn" Binding="{Binding Path=DaysToManufacture}" Header="Days To Manufacture" Width="SizeToHeader" />
11: <DataGridTemplateColumn x:Name="discontinuedDateColumn" Header="Discontinued Date" Width="SizeToHeader">
12: <DataGridTemplateColumn.CellTemplate>
13: <DataTemplate>
14: <DatePicker SelectedDate="{Binding Path=DiscontinuedDate, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" />
15: </DataTemplate>
16: </DataGridTemplateColumn.CellTemplate>
17: </DataGridTemplateColumn>
18: <DataGridTextColumn x:Name="errorColumn" Binding="{Binding Path=Error}" Header="Error" IsReadOnly="True" Width="SizeToHeader" />
19: <DataGridCheckBoxColumn x:Name="finishedGoodsFlagColumn" Binding="{Binding Path=FinishedGoodsFlag}" Header="Finished Goods Flag" Width="SizeToHeader" />
20: <DataGridTextColumn x:Name="listPriceColumn" Binding="{Binding Path=ListPrice,Mode=TwoWay,ValidatesOnDataErrors=True,NotifyOnValidationError=True}" Header="List Price" Width="SizeToHeader" />
21: <DataGridCheckBoxColumn x:Name="makeFlagColumn" Binding="{Binding Path=MakeFlag}" Header="Make Flag" Width="SizeToHeader" />
22: <DataGridTemplateColumn x:Name="modifiedDateColumn" Header="Modified Date" Width="SizeToHeader">
23: <DataGridTemplateColumn.CellTemplate>
24: <DataTemplate>
25: <DatePicker SelectedDate="{Binding Path=ModifiedDate, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" />
26: </DataTemplate>
27: </DataGridTemplateColumn.CellTemplate>
28: </DataGridTemplateColumn>
29: <DataGridTextColumn x:Name="nameColumn" Binding="{Binding Path=Name}" Header="Name" Width="SizeToHeader" />
30: <DataGridTextColumn x:Name="productIDColumn" Binding="{Binding Path=ProductID}" Header="Product ID" Width="SizeToHeader" />
31: <DataGridTextColumn x:Name="productLineColumn" Binding="{Binding Path=ProductLine}" Header="Product Line" Width="SizeToHeader" />
32: <DataGridTextColumn x:Name="productModelIDColumn" Binding="{Binding Path=ProductModelID}" Header="Product Model ID" Width="SizeToHeader" />
33: <DataGridTextColumn x:Name="productNumberColumn" Binding="{Binding Path=ProductNumber}" Header="Product Number" Width="SizeToHeader" />
34: <DataGridTextColumn x:Name="productSubcategoryIDColumn" Binding="{Binding Path=ProductSubcategoryID}" Header="Product Subcategory ID" Width="SizeToHeader" />
35: <DataGridTextColumn x:Name="reorderPointColumn" Binding="{Binding Path=ReorderPoint}" Header="Reorder Point" Width="SizeToHeader" />
36: <DataGridTextColumn x:Name="rowguidColumn" Binding="{Binding Path=rowguid}" Header="rowguid" Width="SizeToHeader" />
37: <DataGridTextColumn x:Name="safetyStockLevelColumn" Binding="{Binding Path=SafetyStockLevel}" Header="Safety Stock Level" Width="SizeToHeader" />
38: <DataGridTemplateColumn x:Name="sellEndDateColumn" Header="Sell End Date" Width="SizeToHeader">
39: <DataGridTemplateColumn.CellTemplate>
40: <DataTemplate>
41: <DatePicker SelectedDate="{Binding Path=SellEndDate, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" />
42: </DataTemplate>
43: </DataGridTemplateColumn.CellTemplate>
44: </DataGridTemplateColumn>
45: <DataGridTemplateColumn x:Name="sellStartDateColumn" Header="Sell Start Date" Width="SizeToHeader">
46: <DataGridTemplateColumn.CellTemplate>
47: <DataTemplate>
48: <DatePicker SelectedDate="{Binding Path=SellStartDate, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" />
49: </DataTemplate>
50: </DataGridTemplateColumn.CellTemplate>
51: </DataGridTemplateColumn>
52: <DataGridTextColumn x:Name="sizeColumn" Binding="{Binding Path=Size}" Header="Size" Width="SizeToHeader" />
53: <DataGridTextColumn x:Name="sizeUnitMeasureCodeColumn" Binding="{Binding Path=SizeUnitMeasureCode}" Header="Size Unit Measure Code" Width="SizeToHeader" />
54: <DataGridTextColumn x:Name="standardCostColumn" Binding="{Binding Path=StandardCost}" Header="Standard Cost" Width="SizeToHeader" />
55: <DataGridTextColumn x:Name="styleColumn" Binding="{Binding Path=Style}" Header="Style" Width="SizeToHeader" />
56: <DataGridTextColumn x:Name="weightColumn" Binding="{Binding Path=Weight}" Header="Weight" Width="SizeToHeader" />
57: <DataGridTextColumn x:Name="weightUnitMeasureCodeColumn" Binding="{Binding Path=WeightUnitMeasureCode}" Header="Weight Unit Measure Code" Width="SizeToHeader" />
58: </DataGrid.Columns>
59: </DataGrid>
60: </Grid>
61: </Window>
The most important part is on line 20. The binding for the ListPrice property is set to TwoWay, ValidatesOnDataErrors is true and NotifyOnValidationError is also true. This will make sure that when the user enters a listprice smaller than zero, validation will kick in:
The WCF Service
Last up is the WCF Service, which uses the new EF 4.1 DbContext api to load from- and update data in the database:
1: using System;
2: using System.Linq;
3: using System.Runtime.Serialization;
4: using System.ServiceModel;
5: using System.ServiceModel.Activation;
6: using EntitiesLibrary;
7: using DbContextLibrary;
8: using System.Collections.Generic;
9: using System.Data.Entity.Validation;
10: using System.Data;
11:
12: namespace ProductsApp.Web
13: {
14: [ServiceContract(Namespace = "")]
15: [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
16: public class ProductService
17: {
18: [OperationContract]
19: public Product[] GetProducts()
20: {
21: using (AdventureWorks2008Entities ents = new AdventureWorks2008Entities())
22: {
23: return ents.Product.AsNoTracking().ToArray();
24: }
25: }
26:
27: [OperationContract]
28: [FaultContract(typeof(string[]))]
29: public void UpdateProduct(Product toUpdate)
30: {
31: using (AdventureWorks2008Entities ents = new AdventureWorks2008Entities())
32: {
33: //prevent validating before save because I do this manually
34: ents.Configuration.ValidateOnSaveEnabled = false;
35:
36: //Prevent detecting changes before save....
37: ents.Configuration.AutoDetectChangesEnabled = false;
38:
39: ents.Product.Attach(toUpdate);
40: ents.ChangeTracker.Entries<Product>().
41: Single((entry) => entry.Entity == toUpdate).State = EntityState.Modified;
42: DbEntityValidationResult error = ents.GetValidationErrors().SingleOrDefault();
43: if (error != null)
44: {
45: throw new FaultException<string[]>(
46: error.ValidationErrors.Select((er) => er.PropertyName + ": " + er.ErrorMessage).ToArray()
47: );
48: }
49: else
50: {
51:
52: ents.SaveChanges();
53: }
54: }
55: }
56: }
57: }
- Line 19-25: This isn’t that exciting, I just load the data with no tracking from the database. In a service, tracking won’t do you any good.
- Line 29-55: This is the most interesting part. My web project also references my EntitiesLibrary project, witch contains the Product class with the buddy mechanism and the data annotation on the listprice. The DbContext api will by default automatically validate before saving and will throw an exception if there are errors. I’m disabling this behavior on line 34 since I’m going to validate up front. Because I’m in a service, the DbContext can’t detect any changes since it doesn’t have any original values, so I’m also disabling this behavior on line 37. On line 39 the product is attached. On line 40-41 I’m changing the state of the product to modified. This is necessary, if you forget this, the DbContext won’t validate (even when doing this manually) or save the product. Since I know for certain that the DbContext is only tracking one entity, I retrieve the single DbEntityValidationResult on line 42. If there are no errors, I will get an empty collection and because of the SingleOrDefault method I will get null. Thus, if the error is not null on line 43, I throw a new FaultException and supply it with a string[]. This string[] contains the validation errors for all the properties that caused errors. Of course, this will only be the listprice since it’s the only one with a data annotation. If the validation returned null, I’m saving the changes on line 52.
The Console Application
As you can see above, using the DbContext api means that you don’t have to write validation code on both the client and the server. This problem was already solved for Silverlight by WCF RIA Services, but now we can finally share the data annotations between the server and other client technologies as well. Now let’s test our service with a client that doesn’t do any validation in the UI and tries to submit some illegal values:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using ProductsConsoleApp.Services;
6: using EntitiesLibrary;
7: using System.ServiceModel;
8: using System.ComponentModel;
9:
10: namespace ProductsConsoleApp
11: {
12: class Program
13: {
14: static void Main(string[] args)
15: {
16: ProductServiceClient client = new ProductServiceClient();
17: Product first = client.GetProducts()[0];
18: first.ListPrice = -1;
19: try
20: {
21: client.UpdateProduct(first);
22: }
23: catch (FaultException<string[]> ex)
24: {
25: Console.WriteLine("There were validation errors on the server:");
26: foreach (string errorMessage in ex.Detail)
27: {
28: Console.WriteLine(errorMessage);
29: }
30: }
31: Console.ReadKey();
32: }
33: }
34: }
If you’d run the application above you would get the following output:
And we can see that our server is protected against bad input.
Conclusion
One of the most overlooked features of the Entity Framework 4.1 is the new ability to validate the entities according to the buddy class mechanism and data annotations. This means that when we write serverside code, we don’t have to write the validation logic ourselves and that we can share the entities between client and server to use the same validation logic on the client. This also works of course when the client is an ASP.NET MVC Controller for example.
I’ve tried to share code this way between a Silverlight client and a WCF Service but this doesn’t work. This is because the EntitiesLibrary project must be a Silverlight class library before it can be referenced by both Silverlight and the server project. In Silverlight the data annotations are defined in a different assembly with a different version than the data annotations in the full .Net framework. The Entity Framework and the Validator class in the full .Net framework will NOT recognize data annotations from Silverlight. It seems that the best way to share validation code with Silverlight is by using WCF RIA Services. You can find the working example here.