Contents
Background
By default, all entity classes in Entity Framework are generated inheriting the EntityObject
class. This makes them dependent on EF. With release 4.1,
Microsoft provided a way of creating persistence ignorant classes: just Plain Old CLR Objects (POCO). These objects learn about persistence only when they
are attached to an entity container. This is done through the magic of proxy classes that are dynamically derived from the entity classes. Microsoft did not
hide the generation of these classes in the model designer, but instead provided text transform files. So if you don’t like the way Microsoft generated the entity classes,
just change the transform files! That’s what I did. I also created conceptual schema annotation attributes that let you selectively turn on or off my code generation.
Then I put this all together in a project item template so that you can just add my template instead of the Microsoft one. Microsoft provides both a C# version and a VB version.
I only made a VB version.
Introduction
What I Changed
Following is a list of the changes I made.
- All properties are
virtual
(overridable
in VB speak). This makes the entities support automatic change tracking. - Documentation elements in the conceptual schema are copied to XML help text.
Summary
to Summary
and LongDescription
to Remarks
. - Before a property changes, the code invokes a partial method
pOn[PropertyName]Changing
where you can do your property validation. - After a property changes, the code invokes a method that will raise the
PropertyChanged
event for WPF notification. - Also after change, the code invokes a partial method
pOn[PropertyName]Changed
where you can do further change notification. - Public constants are created to hold the value of the property facets
MaxLength
, Precision
, or Scale
(only if the property has them). - Before an entity updates the database or is inserted into the database, the code invokes a partial method
pValidate[EntityName]
where you can do entity level validation.
Required features
Notifying and validating at the property level is not as straightforward as it might seem. You don’t always want to do them. Following is a list of cases when you don’t:
- When the entities are being loaded from the database.
- When you create a new entity object and initialize some of the properties.
- But you do want to, after you add the new entity object to the entity set.
- When you cancel a pending update. You still want to notify the changes but not validate them.
- When you do batch updates. This is for performance reasons. You may still want to validate depending on how well you trust the batch code.
Required code
In order to support the required features and raise the property changed event, I require all entity and complex classes to inherit the EntityBase
class.
This class must have these three members:
- A public Boolean property
IsNotifyEnabled
- A public Boolean property
IsValidationEnabled
- A method named
pRaisePropertyChanged
that will raise the PropertyChanged
event with the following signature:
Protected Sub pRaisePropertyChanged( args As PropertyChangedEventArgs)
A sample class is included in the template. This class is not generated. It’s just copied. So you can modify it or delete it if you already have a class that
can support these members. If you don’t like the name EntityBase
, you can change the two text transform files "Types.tt" and "Container.tt".
Somewhere close to the beginning of each file, you will find:
Dim entityBaseName As String = "EntityBase"
If you don’t like the member names, you can do a "replace all". If you are unfamiliar with these text transform files, a good introduction
is T4 Templates and the Entity Framework(^) from Microsoft's Data Development Center.
A look at the generated code
Non-complex property generated code
The setter code for the string property named Title
will look like this:
Set(ByVal value As String)
Dim oldValue As String
Dim newValue As String
If pIsTitleInitialized Then
If pTitle Is value Then Exit Property
oldValue = pTitle
newValue = value
If IsValidationEnabled Then
pOnTitleChanging(oldValue, newValue)
End If
pTitle = newValue
If IsNotifyEnabled Then
pRaisePropertyChanged(pTitlePropertyArgs)
pOnTitleChanged(oldValue, newValue)
End If
Else
pIsTitleInitialized = True
pTitle = value
End If
End Set
The beginning of this code checks the field pIsTitleInitialized
. This is initialized to false. If you look near the bottom after the "Else",
you will see that it turns this on and saves the value. So, if the property is not initialized, it won’t notify or validate. This satisfies
our Required features 1 and 2 (don’t do anything extra when loading from database or initializing a new entity).
Once we decide that we’re initialized, we do:
pTitle Is value Then Exit Property
which exits if nothing changed. The code generated for this depends on the type of the property. Strings and entity references use "Is
". All other non-nullable types
use "=
". Nullable types do something different. For example, a property named "PubYear
" with type "Integer
" will generate:
If pPubYear = value OrElse
(pPubYear Is Nothing AndAlso value Is Nothing) Then Exit Property
Once we decide that the property will actually change, we check "IsValidationEnabled
" and later on "IsNotifyEnabled
".
These are the properties from EntityBase
. As you can see, if we set these to false, we can stop validation and/or notification. This satisfies
our Required features 4 and 5.
That leaves us with requirement 3. We need a way to insure all properties are initialized before we add a new entity to its entity set. To accomplish this,
entity and complex classes get a method which will set each property’s pIs[PropertyName]Initialized
field or call the complex property’s method.
So, all we have to do before adding a new entity is newEntity.SetInitialized(True)
.
About complex properties
Complex properties are never validated. The properties in the object they reference are validated. Complex properties shouldn’t be changed.
The properties in the object they reference are changed. But a complex property can be displayed if the complex class overrides ToString
with something meaningful.
You can even use one as a sort column if the class implements IComparable
. So we might want to be notified if any of the properties in the referenced object changes.
The generated code will do this, but it makes one assumption: that each complex object exists in one and only one property in one and only one object. This should be true
if you don’t change a complex property. By making this assumption, we can say that a complex property "owns" its complex object.
Complex class extra generated code
A complex class gets extra code to handle notifying its "owner".
Friend Property RaiseOwnerPropertyChanged As Action = Sub() Exit Sub
Private Sub pRaiseMineAndOwnerPropertyChanged(
arg As PropertyChangedEventArgs)
pRaisePropertyChanged(arg)
RaiseOwnerPropertyChanged.Invoke()
End Sub
When properties in entity classes want to raise the property changed event, they call the EntityBase
method pRaisePropertyChanged
.
Properties in complex classes do something different: they call this routine. This then calls the EntityBase
method and then invokes the action
RaiseOwnerPropertyChanged
, which will be set by the property owner if it wants to be notified. If it does not, the action defaults to a do-nothing "Exit Sub
".
Complex property generated code
The code for the complex property "AuthorName
" of type "FullName
" in entity "Book" looks like this:
Public Overridable Property AuthorName As FullName
Get
If pAuthorName Is Nothing Then
pAuthorName = New FullName
pAuthorName.RaiseOwnerPropertyChanged =
Sub()
pRaisePropertyChanged(pAuthorNamePropertyArgs)
pOnAuthorNameChanged(pAuthorName)
End Sub
End If
Return pAuthorName
End Get
Protected Set(ByVal value As FullName)
pAuthorName = value
pAuthorName.RaiseOwnerPropertyChanged =
Sub()
pRaisePropertyChanged(pAuthorNamePropertyArgs)
pOnAuthorNameChanged(pAuthorName)
End Sub
End Set
End Property
Private pAuthorName As FullName
Private Shared ReadOnly pAuthorNamePropertyArgs As New _
PropertyChangedEventArgs("AuthorName")
Partial Private Sub pOnAuthorNameChanged(complexObject As FullName)
End Sub
Some notes on the code:
- The setter is "
Protected
". This is as close to "read only" as we can get because we must allow the entity proxies access. "Protected
" was specified
in the entity model. By default, notification code for complex properties won’t be generated unless the setter is Protected
. - The getter creates a new complex object if one does not exist. This happens even if the notification code is not generated.
- Both the getter and setter set the complex object’s
RaiseOwnerPropertyChanged
action to call our EntityBase
method and then
call a partial method that you can optionally code for further notification logic.
Entity validation generated code
Protected Overrides Function ValidateEntity(
entityEntry As DbEntityEntry,
items As IDictionary(Of Object, Object)
) As DbEntityValidationResult
Dim mErrors = New List(Of DbValidationError)()
If DirectCast(entityEntry.Entity, EntityBase).IsValidationEnabled Then
Select Case entityEntry.Entity.GetType.BaseType
Case GetType(Book)
pValidateBook(entityEntry.Cast(Of Book), mErrors)
. . . .
. . . .
End Select
End If
Dim mResult = New DbEntityValidationResult(entityEntry, mErrors)
Return mResult
End Function
Some notes on the code:
- The entity is cast to
EntityBase
so that it can check if validation is enabled. - The
Select
statement gets the type of the entity and then gets its base type. It does this because the entity to be validated
is a proxy entity and we want the "real" entity type.
How to validate
How to do property validation
Let’s take an example of a string property named "Title
" in an entity named "Book". (Or it could be a property in a complex class. The process is the same.)
You first create a class named "Book
" to go with the generated partial class. Next, within this class, you create a method:
Private Sub pOnTitleChanging(
ByVal oldValue As String,
ByRef newValue As String)
This overrides the partial method generated in the partial class. You can let VB generate this for you. If you pull down the list at the top right of the VB editor,
you will see all of the partial methods grayed out. Just click the one you want, and VB will insert it for you.
Your code must throw an exception when it finds something not valid. To let WPF know about this, your XAML code can look like this:
<TextBox Text="{Binding Title,
ValidatesOnExceptions=True,
NotifyOnValidationError=True}" />
Sometimes it’s better to fix something invalid rather than pester the user. This you can do by just changing the parameter newValue
.
You will notice that it is passed ByRef
instead of ByVal
.
Since this is a string property, you have access to the constant TitleMaxLength
, which was generated from the conceptual model. If this were a decimal
property, you would have constants [PropertyName]Precision
and [PropertyName]Scale
.
Remember that this class is a persistence ignorant class. This means that the only data you have access to is the data in this instance and any entity object
accessible through the navigation properties. For example, you cannot check for duplicates against an entity set. To do that, you need entity level validation.
How to do entity validation
Entity validation happens in the entity container rather than in the entity type. This is where all the persistence happens. The name of this class is defined
in your conceptual model with the property "Entity Container Name". Like an entity class, you have to create your own to match the generated partial class.
To validate our "Book" entity that we used in property validation, you add this method to your entity container class:
Private Sub pValidateBook(
entityEntry As DbEntityEntry(Of Book),
validationErrors As ICollection(Of DbValidationError))
Note that the entityEntry
parameter is specific to "Book" entities. This is the object that Entity Framework uses to keep track of changes.
From this, you can find out whether the validation is due to modification or addition (deletes aren’t validated). You can also check whether a specific property
within "Book" was modified. If you find something invalid, you add an error object to the parameter validationErrors
.
This routine gets invoked from SaveChanges
. If it finds the parameter validationErrors
is not empty, it will throw a DbEntityValidationException
,
which contains all the error messages from all the validated entities. You can also invoke entity validation by calling GetValidationErrors
.
Controlling what gets generated
What’s Annotation?
Microsoft allows you to annotate your conceptual schema with additional attributes and elements. I took advantage of this by defining two boolean
attributes: GenerateEntityValidation
and GeneratePropertyNotify
. These let you turn on or off generating the extra code by entity type and property.
GenerateEntityValidation attribute
By default, the partial method for entity validation is generated for all entity types that are not abstract (MustInherit
in VB speak). You can stop this for a particular entity type by setting this attribute to "false". If you set this attribute on the entity container to "false",
then the validation method is generated only for those with this attribute set to "true" and not abstract.
GeneratePropertyNotify attribute
By default, the notification and validation code is generated for all properties and navigation properties in all entity types and complex types, except the following:
- Navigation properties with the principal role (an entity collection property).
- (Primary) key properties.
- Foreign key properties.
- Updatable complex properties (setter access of
public
or internal
). - "Read only" non-complex properties (setter access of
private
or protected
).
To have the extra code generated for a property that it does not do by default, set the attribute to "true" on that property (except navigation entity collection properties).
Or to turn off extra code generation for a property, set it to "false". If you set this attribute to "false" on an entity type or complex type, then the only properties
within that type with extra generated code are those with this attribute set to "true".
How to annotate your conceptual schema
To annotate your conceptual schema, you need to open you entity model with the XML editor instead of the design editor. To do this:
- Right click your entity model file (.edmx)
- Pick "Open With …"
- Pick "XML (Text) Editor"
The first schema you will see is the storage schema. If you collapse this, you see the conceptual schema that looks like this:
<edmx:ConceptualModels>
<Schema Namespace="YourModel" Alias="Self"
xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation"
xmlns=http://schemas.microsoft.com/ado/2008/09/edm>
You need to add an XML namespace to the schema element:
xmlns:code="http://EntityModel.Annotations.Notify"
You will now get IntelliSense help for my attributes under "code:". This happen because my template added the XML schema file [ModelFileName].Annotations.Notify.xsd.
The template
Prerequisite
This template requires at least 4.1 of Entity Framework. If you don't have it installed, you can get it
at Microsoft's Download
Center(^).
The files added by the template
Four files get added by the template. Each file name is prefaced by your model file name (the .edmx file):
- [ModelFileName].Annotations.Notify.xsd
This is the schema of my annotation attributes. It provides IntelliSense when you annotate your conceptual schema.
- [ModelFileName].Container.tt
This is the text transform file that generates the partial entity container class. This class inherits from the new 4.1 DBContext
class
rather than ObjectContext
.
- [ModelFileName].EntityBase.vb
This is the sample EntityBase
class. See required code.
- [ModelFileName].Types.tt
This is the text transform file that generates all of the entity and complex partial classes.
Installing the template
To install the template, download the template (9.62 KB) and copy it to your
folder: "Visual Studio 2010 /Templates /Item Templates /Visual Basic /Code". You may have to create the last folder. Don't unzip it. Template files are zip files.
Using the template
If you are still using the default code generation strategy (that is, your entities are inheriting from EntityObject
), then you first need to turn this off:
- Open your entity model in design mode.
- Right click on some blank space and pick Properties.
- The first item in Properties should be "Code Generation Strategy". Set this to "none".
If you are already using Microsoft's template, you need to delete their generated files:
- [ModelFileName].Context.tt
- [ModelFileName].tt
If you skip these first steps, you wind up with duplicate classes. You can still do the steps after, but doing them first avoids all of those errors.
Now you're ready to add the new template:
- Right click the folder that contains your entity model (.edmx) and pick Add /New Item...
- Under "Installed Templates", expand "Common Items" and pick "Code".
- You should see the new template as the first one titled "ADO.NET DbContext Generator with Notify". Click it.
- If you look to the right of the list, you will see that it's telling you that the name of this item must be the same as your entity model file.
- Once you have named it correctly, click Add.
- The four files will be added and all of the classes generated. This may take a few seconds, depending on how many classes you have.
If you have used Microsoft's template, you know that you pick theirs from the model's context menu "Add Code Generation Item ...". If you do that, you won't find mine.
I couldn't figure out how to get in this select group. So I have to depend on you spelling the item correctly. If you don't, you will get some nasty errors complaining,
among other things, that it can't find the file.
History
- Posted October 19, 2011.