In the previous chapter you learned many ways that you can apply
validation rules so that the DbContext Validation API can find and check
them either on demand or automatically. While you can explicitly validate
individual classes and properties directly from the DbEntityEntry
method, you can also have the
context validate all of its tracked entities as a group, either by calling
GetValidationErrors
or letting SaveChanges
call that method for you. GetValidationErrors
then calls ValidateEntity
on each of the Added
and Modified
entities in the context. ValidateEntity
then triggers logic that checks the
ValidationAttribute
and IValidatableObject
rules you’ve specified in your
classes.
You’ve seen how ValidateEntity
works in Chapter 6. In this
chapter, you’ll learn how to customize the ValidateEntity
method not only by overriding the
logic of the method, but also by overriding the method that determines which
entities should be validated.
ValidateEntity
is a virtual
method, meaning that you can override it and add your own custom logic.
Like any virtual method, after executing your logic, you can control
whether or not it performs the validations it’s designed to execute (for
example, validating the ValidationAttribute
s and IValidatableObject
rules).
Example 7-1 shows the
ValidateEntity
method added into the
BreakAwayContext
class after using the
Visual Studio IDE shortcut to add the overridden method.
protected override
System.Data.Entity.Validation.DbEntityValidationResult
ValidateEntity( System.Data.Entity.Infrastructure.DbEntityEntry
entityEntry, System.Collections.Generic.IDictionary
<object
,object
> items) {return base
.ValidateEntity
(entityEntry, items); }
If you are new to overriding methods in Visual Studio, the IDE has
a shortcut to help you insert the method in C# and in VB. In the
Break
Away
Context
class, type the word override
(Overrides
for VB) followed by a space. Visual
Studio will then show you a list of virtual methods. Select ValidateEntity
from the list and the method
code will be automatically added to the class.
By ensuring that the System.Data.Entity.Infrastructure,
System.Data.Entity.Validation
, and System.Collections.Generic
namespaces are all
added to the using
statements at the
top of the class file, the method signature becomes a little easier to
read:
protected override
DbEntityValidationResult
ValidateEntity
(DbEntityEntry
entityEntry,IDictionary
<object
,object
> items) {return base
.ValidateEntity
(entityEntry, items); }
You can add logic to ValidateEntity
that performs additional
validations on all types or on a subset of types (for example, a
particular type or a particular set of types that inherit from another
class or implement from an interface).
Another benefit of inserting logic here is that you have access to
the DbContext
and therefore can perform
validation that depends on other tracked entities or even checks against
data in the database. That’s something you can’t do in a ValidationAttribute
or in the IValidatableObject.Validate
method unless you
were to pass a DbContext instance into the type. This would, however,
force the type to be aware of the data layer which, if you care about
keeping your POCO classes persistence ignorant, is
undesirable.
You can read more about persistence ignorance (PI) from the perspective of Entity Framework in Chapter 24 of Programming Entity Framework, 2e, or in any number of resources on the Internet. Here, for example, is a discussion of PI in the scope of an article on the Unit of Work Pattern and Persistence Ignorance by Jeremy Miller in MSDN Magazine: http://msdn.microsoft.com/en-us/magazine/dd882510.aspx#id0420053.
An example of a validation that involves multiple entities is a rule
for BreakAway Geek Adventures that a payment must made along with a new
reservation. You’ll find a Payment
class in the model of the sample download along with a Payments
navigation property in the Reservation
class. The two classes are listed in
Example 7-2.
public class
Payment
{public
Payment() { PaymentDate =DateTime
.Now; }public int
PaymentId {get; set;
}public int
ReservationId {get; set;
}public
DateTime
PaymentDate {get; set;
}public decimal
Amount {get; set;
} }public class
Reservation
{public
Reservation() { Payments =new
List
<Payment
>(); }public int
ReservationId {get; set;
}public
DateTime
DateTimeMade {get; set;
}public
Person
Traveler {get; set;
}public
Trip
Trip {get; set;
}public
Nullable
<DateTime
> PaidInFull {get; set;
}public
List
< > Payments {get; set;
} }
You may want your custom context logic to take precedence over the
other validations that would be performed. In other words if the custom
logic added in ValidateEntity
fails,
then don’t bother validating the rules that are specified in ValidationAttributes
or IValidatableObject
. If no errors are detected in
the custom context logic, then the base.ValidateEntity
method will get called to
check rules defined with ValidationAttributes
and IValidatableObject
. Figure 7-1 helps you visualize
this workflow. You’ll explore a number of other possible workflows later
in the chapter.
The ValidateEntity
signature
contains an entityEntry
parameter. This
represents the DbEntityEntry
for the
object currently being processed by the SaveChanges
method. DbEntityEntry
allows you to navigate to the
actual object instance that it represents. You cast with the as
operator to ensure you are working with the
correct type:
var
reservation = entityEntry.Entityas
Reservation
;if
(reservation !=null
) {//logic on reservation goes here
}
From here you can use the reservation
instance or work directly against
the change tracker through entityEntry
.
Example 7-3 shows
code that validates the new rule for Reservation.
The code instantiates a new
DbEntityValidationResult
for this
particular entry. Then, if the entry is for a Reservation
and is new (Added
) but has no Payments
, a new error is added to the DbEntityValidationResult
. If the reservation
validation results in errors (in which case, result.IsValid
will be false
), those results are returned from ValidateEntity
and the base validation is not
called. If the result is valid, the base method is called instead.
Remember from Chapter 6 that ValidateEntity
temporarily disables lazy
loading, so the context will not be looking for any payments in the
database.
protected override
DbEntityValidationResult
ValidateEntity
(DbEntityEntry
entityEntry,IDictionary
<object
,object
> items) {var
result =new
DbEntityValidationResult
(entityEntry,new
List
<DbValidationError
>());var
reservation = entityEntry.Entityas
Reservation
;if
(reservation !=null
) {if
(entityEntry.State ==EntityState
.Added && reservation.Payments.Count == 0) { result.ValidationErrors.Add(new
DbValidationError
("Reservation"
,"New reservation must have a payment."
) ); } }if
(!result.IsValid) {return
result; }return base
.ValidateEntity
(entityEntry, items); }
Keep in mind an important detail of the processing steps described
earlier in Chapter 6.
GetValidationErrors
(called by
SaveChanges
) will execute ValidateEntity
on all of the tracked entities
before it begins constructing commands for the database. When designing
custom logic for ValidateEntity
,
don’t expect entities that have already been validated to be in the
database by the time you reach the next entity.
If you have multiple validations to perform in ValidateEntity
, it could get cluttered up pretty
quickly. Example 7-4 shows the
same logic as Example 7-3,
but with the validation specific to the Reservation
split out to a separate
method.
protected override
DbEntityValidationResult
ValidateEntity (DbEntityEntry
entityEntry,IDictionary
<object
,object
> items) {var
result =new
DbEntityValidationResult
(entityEntry,new
List
<DbValidationError
>()); ValidateReservation(result);if
(!result.IsValid) {return
result; }//call base validation
return base
.ValidateEntity(entityEntry, items); }private void
ValidateReservation(DbEntityValidationResult
result) {var
reservation = result.Entry.Entityas
Reservation
;if
(reservation !=null
) {if
(result.Entry.State ==EntityState
.Added && reservation.Payments.Count == 0) { result.ValidationErrors.Add(new
DbValidationError
("Reservation"
,"New reservation must have a payment."
) ); } } }
In the previous example, ValidateEntity
executes our context-based
business validations. If no errors are found, it continues on to execute
the base ValidateEntity
method, which
checks any rules defined with type validation (IValidatableObject
rules) and property
validation (ValidateAttribute
rules).
That’s just one execution path you could set up in ValidateEntity
.
Throughout this chapter, we’ll present different forms of the
ValidateEntity
method. If you are following along
with the code samples, you might want to retain each version of
ValidateEntity
in the
BreakAwayContext
class. What we did while developing
our samples was to wrap a complier directive around the methods that we
don’t want to use anymore. This is cleaner than commenting out code. In
C# you can add #if false
before the
beginning of the method and then #endif
after the end of the method.
#if
false protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items) { ...method code }#endif
The code inside the directive will be grayed out and ignored by
the compiler. Change the directive to #if
true
to reengage it.
In Visual Basic the directive looks like this:
#If False Then
#End If
You could reverse this logic, returning the base ValidateEntity
results first and, if there are
none, executing your custom logic as visualized in Figure 7-2.
As an example, you might want to check a value for uniqueness in the
database, perhaps to ensure that new Lodgings
have a unique Name
and Destination
combination. You can do this in
ValidateEntity
because you have access
to the context and therefore can execute a query such as
Lodgings.Any(l => l.Name == lodging.Name && l.DestinationId == lodging.DestinationId);
But Lodging.Name
already has a
number of ValidationAttribute
rules
applied: Required
, MinLength
, and MaxLength
. You might prefer to ensure that these
three attributes are satisfied before wasting the trip to the database to
check for a duplicate lodging. You could run the base ValidateEntity
method first and return its
errors if there are any. If there are no errors found in the base
validation, continue on to the new validation logic, which checks the
database for an existing lodging with the name and destination of the one
about to be added. Example 7-5 demonstrates this
logic. First, base.ValidateEntity
is
called. If its results are valid, a custom validation method, ValidateLodging
, is called and its errors, if
any, are added to the results collection, which is returned at the
end.
protected override
DbEntityValidationResult
ValidateEntity (DbEntityEntry
entityEntry,IDictionary
<object
,object
> items) {var
result =base
.ValidateEntity(entityEntry, items);if
(result.IsValid) { ValidateLodging(result); }return
result; }private void
ValidateLodging(DbEntityValidationResult
result) {var
lodging = result.Entry.Entityas
Lodging
;if
(lodging !=null
&& lodging.DestinationId != 0) {if
(Lodgings.Any(l => l.Name == lodging.Name && l.DestinationId == lodging.DestinationId)) { result.ValidationErrors.Add(new
DbValidationError
("Lodging"
,"There is already a lodging named "
+ lodging.Name +" at this destination."
) ); } } }
Checking for uniqueness in Example 7-5 may have made you wonder about a simpler way to define unique validations. The Entity Framework team is working on a feature that would allow you to define Unique Constraints directly in the model. You can read more details about this in their March 2011 blog post at http://blogs.msdn.com/b/efdesign/archive/2011/03/09/unique-constraints-in-the-entity-framework.aspx.
We created a method in the console app called CreateDuplicateLodging
to test this validation,
shown in Example 7-6.
private static void
CreateDuplicateLodging() {using
(var
context =new
BreakAwayContext
()) {var
destination = context.Destinations .FirstOrDefault(d => d.Name =="Grand Canyon"
);try
{ context.Lodgings.Add(new
Lodging
{ Destination = destination, Name ="Grand Hotel"
}); context.SaveChanges();Console
.WriteLine("Save Successful"
); }catch
(DbEntityValidationException
ex) {Console
.WriteLine("Save Failed: "
);foreach
(var
error in ex.EntityValidationErrors) {Console
.WriteLine(string
.Join(Environment
.NewLine, error.ValidationErrors.Select(v => v.ErrorMessage))); }return
; } } }
The critical part of this method inserts Grand Hotel at the Grand
Canyon while the bulk of the method is code to display errors for this
demonstration. Our seed data includes a Lodging
called Grand Hotel at the Destination
Grand Canyon. So our new Lodging
will be a duplicate. If you run this
method from the console application’s main method, ValidateEntity
will call ValidateLodging
and discover the duplication.
The console will report the error:
Save Failed: There is already a lodging named Grand Hotel at this destination.
Now let’s add in a validation that will fail the base.ValidateEntity
check. Modify the Lodging to
add a data annotation to the MilesFromNearestAirport
property. The Range
Attribute
specifies a valid value range for
the property. Here we’ll say that anything from .5 to 150 miles will be
valid:
[Range
(.5,150)]public decimal
MilesFromNearestAirport {get
;set
; }
If you run the application again, you’ll see this message in the console window:
Save Failed: The field MilesFromNearestAirport must be between 0.5 and 150.
There’s no mention of the duplication. That’s because the ValidateEntity
method is designed to check the
property and type rules first and return the exception right away if any
are found—before it has called ValidateLodging
.
Let’s return to the ValidateEntity
method and force it to return the
combination of validation errors checked in the custom logic and in the
logic check by base
.
Validate
Entity
, as visualized in Figure 7-3.
Example 7-7
demonstrates code that will allow you to collect the results of the base
validation and then add any additional errors found in the custom logic to
that result before returning the combined errors from ValidateEntity
.
protected override
DbEntityValidationResult
ValidateEntity (DbEntityEntry
entityEntry,IDictionary
<object
,object
> items) {var
result =base
.ValidateEntity(entityEntry, items); ValidateLodging(result);return
result; }
Running the CreateDuplicateLodging
method one last time will
now display both errors:
Save Failed: The field MilesFromNearestAirport must be between 0.5 and 150. There is already a lodging named Grand Hotel at this destination.
You can include multiple validation checks in ValidateEntity. These examples only contain one at a time for the sake of brevity.
Now that you’ve seen a few possible workflows for executing
validations in Validate
Entity
, you can mimic these or define your
own workflow when customizing ValidateEntity
.
Quite often, there are last-minute modifications that you want to
make to data before it’s sent to the database. One example is setting
DateAdded
and DateModified
values in your classes. While there
are a number of ways to achieve this in .NET code, you may wish to perform
this logic in the data layer. Because the context is already iterating
through all of its Added
and Modified
entities when it calls ValidateEntity
, it’s tempting to add this logic
into ValidateEntity
rather than perform
an additional enumeration in the SaveChanges
method.
It’s possible to do this, but it is not recommended. The following are some downsides to putting this type of logic inside of ValidateEntity:
The ValidateEntity
method is
designed for performing validations. Using it for other purposes
infringes on the principle of Singular Responsibility—a coding
principle that exists to help you in the quest for maintainable
code.
You might not want to bother with modifying data before you know
it’s valid and headed for the database. You could call base.ValidateEntity
prior to the update
logic as shown in Example 7-5, but a later
entity might be invalid, rendering all modifications moot.
By the time you’re in the ValidateEntity
method, DetectChanges
has already been called and
you need to be careful about how you update values.
One alternative is to override SaveChanges
and iterate through entities to
apply the dates before base.SaveChanges
does a second iteration to validate the entities. Keep in mind that after
this, there is a third iteration—the one that creates and executes the
commands for each Added
, Modified
, and Deleted
entity to the database.
If you override SaveChanges
to
apply the date values, ValidateEntity
will be called afterwards, during base.SaveChanges
. If invalid data is found, the
effort and processing time taken to update the date properties was
wasted.
In the next section, we’ll look at pros and cons of the options you
have to perform the date modifications during SaveChanges
and then show you an efficient
example of modifying ModifiedDate
and
AddedDate
properties during
SaveChanges
.
If you want to set values inside of SaveChanges and you are leveraging the Validation API, you have a number of choices:
Update the data values in SaveChanges and let base.SaveChanges
perform the validation
(through ValidateEntity
) as it
normally would.
Turn off ValidateOnSaveEnabled
and iterate through
entities, calling GetValidationResult
and then the date fix-up
for each entity.
Turn off ValidateOnSaveEnabled
and iterate through
entities, fixing up the dates for each entity and then calling
GetValidationResult
.
Turn off ValidateOnSaveEnabled
. Call GetValidationErrors
(which will iterate
through entities) and then iterate again performing the date
fix-ups.
Turn off ValidateOnSaveEnabled
. Iterate through
entities to perform the date fix-ups, and then call GetValidationErrors
(which will iterate
through entities). This would be no different than the first option in
this list.
There are pros and cons to each approach. We’ll walk through one of them and present the pros and cons of the others. In the end, you should be able to choose the approach that best fits your application.
For this example, we’ll perform validations by calling GetValidationErrors
and then update two date
fields for any entity that inherits a new base class, Logger
. In this scenario, we have confidence
that updating the date fields won’t break any validation rules, so it is
safe to perform this task after the validations have been checked by the
API. In a real-world application, you should have automated tests in place
to ensure that future modifications to the application don’t break this
assertion.
Example 7-8 shows a new abstract class
called Logger
, which exposes two new
date fields. It also has a public method, UpdateModificationLogValues
, for updating those
fields. This method may be used by any number of business logic methods
that access your classes.
public abstract class
Logger
{public
DateTime
LastModifiedDate
{get
;set
; }public
DateTime
AddedDate
{get
;set
; }public void
UpdateModificationLogValues
(bool
isAdded) {if
(isAdded) {AddedDate
=DateTime
.Now
; }LastModifiedDate
=DateTime
.Now
; } }
Modify the Activity
class to
inherit from Logger
:
public class
Activity
:Logger
Now you can override SaveChanges
,
as shown in Example 7-9.
Back in Chapter 5 you overrode
SaveChanges
to perform logging; you should replace the
logging implementation with this new implementation. Notice that the first
action in the method is to store the current setting of AutoDetectChangesEnabled
. That’s because we’re
going to temporarily set it to false
and want to reset it before exiting the method. The reason we’re setting
it to false
is to control exactly when
DetectChanges
is called.
public override int
SaveChanges() {var
autoDetectChanges = Configuration.AutoDetectChangesEnabled;try
{ Configuration.AutoDetectChangesEnabled =false
; ChangeTracker.DetectChanges();var
errors = GetValidationErrors().ToList();if
(errors.Any()) {throw new
DbEntityValidationException
("Validation errors found during save."
, errors); }foreach
(var
entityin this
.ChangeTracker.Entries() .Where(e => e.State ==EntityState
.Added || e.State ==EntityState
.Modified)) { ApplyLoggingData(entity); } ChangeTracker.DetectChanges(); Configuration.ValidateOnSaveEnabled =false
;return base
.SaveChanges(); }finally
{ Configuration.AutoDetectChangesEnabled = autoDetectChanges; } }
The ApplyLoggingData
method shown
in Example 7-10 will call
UpdateModificationLogValues
on any
entities that inherit from Logger
,
passing in a Boolean to signal whether or not the AddedDate
needs to be set. Once all the dates
have been updated, we call DetectChanges
to ensure that
Entity Framework is aware of any changes that were made.
private static void
ApplyLoggingData(DbEntityEntry
entityEntry) {var
logger = entityEntry.Entityas
Logger
;if
(logger ==null
)return
; logger.UpdateModificationLogValues (entityEntry.State ==EntityState
.Added); }
Now we’ll test that the date fields get changed by adding a new,
valid Activity
using the InsertActivity
method (added into the console
application) shown in Example 7-11. The code displays
the dates after SaveChanges
has been
called. After the first call to SaveChanges
, the code makes a modification and
saves again, displaying the dates a second time.
private static void
InsertActivity() {var
activity =new
Activity
{ Name ="X-C Skiing"
};using
(var
context =new
BreakAwayContext
()) { context.Activities.Add(activity);try
{ context.SaveChanges();Console
.WriteLine("After Insert: Added={0}, Modified={1}"
, activity.AddedDate, activity.LastModifiedDate);//pause 2 seconds
System.Threading.Thread
.Sleep(2000); activity.Name = ("X-C Skating"
); context.SaveChanges();Console
.WriteLine("After Modified: Added={0}, Modified={1}"
, activity.AddedDate, activity.LastModifiedDate); }catch
(DbEntityValidationException
ex) {Console
.WriteLine("Save Test Failed: "
+ ex.EntityValidationErrors.FirstOrDefault() .ValidationErrors.First().ErrorMessage); } } }
When you run this, you’ll see that the newly inserted activity has
its AddedDate
and LastModifiedDate
values populated after being
saved. Then when the activity is edited and saved again, you can see that
its LastModifiedDate
value has been
updated again, thanks to the combined logic in SaveChanges
and the Logger
class:
After Insert: Added=12/9/2011 12:16:27 PM, Modified=12/9/2011 12:16:27 PM After Modified: Added=12/9/2011 12:16:27 PM, Modified=12/9/2011 12:16:33 PM
This works because by the time we’re calling ApplyLoggingData
, the context is already aware
that these are either Added
or Modified
entities and is already planning to
persist them to the database.
You can avoid the need to call DetectChanges
by
changing properties directly through the change tracker. That’s logic that
you won’t be able to (or want to) embed into your domain classes (which we
prefer to not have any knowledge of Entity Framework). So you’ll have to
do that within the context. Example 7-12 shows what the
ApplyLoggingData
would look like if you
were to set the properties through the change tracker.
private static void
ApplyLoggingData(DbEntityEntry
entityEntry) {var
logger = entityEntry.Entityas
Logger
;if
(logger ==null
)return
; entityEntry.Cast<Logger
>() .Property(l => l.ModifiedDate).CurrentValue =DateTime
.Now;if
(entityEntry.State==EntityState
.Added) { entityEntry.Cast<Logger
>() .Property(l => l.AddedDate).CurrentValue =DateTime
.Now; } }
In Chapter 5, you learned how to work
with scalars as well as collection and reference navigation properties
through the change tracker. If you want to make changes to navigation
properties at the data layer, you should do so using the change tracker,
as shown with the scalar property changes made in Example 7-12. If you used the
navigation properties of the class directly, whether you do that in the
context code or call into code in the type you are modifying (for example,
Logger.UpdateModificationLogValues
),
you run a substantial risk of those changes not being persisted to the
database. Again, this is dependent on where in the workflow DetectChanges
is being called. If you are in the
habit of using the change tracker to make the changes, you don’t have to
worry about DetectChanges
.
If you’ve been using Entity Framework for a few years, you might
be familiar with various options we’ve had for applying validation logic
and wondering how Validate
Entity
fits into the picture. The first
version of Entity Framework gave us the ObjectContext.SavingChanges
event, which let
developers execute validation or other logic when SaveChanges
was called. Your logic added into
SavingChanges
would be executed and then Entity
Framework would execute its internal logic. Entity Framework 4 brought
us the added benefit of a virtual SaveChanges
method so we could not only have
Entity Framework execute our custom logic when calling SaveChanges
, but we had the option of
completely halting the internal code.
You can also override SaveChanges
when it is called from DbContext
. DbContext
doesn’t have a SavingChanges
event because with the virtual
SaveChanges
, the former approach is
redundant. The only reason SavingChanges
still exists as a method of
ObjectContext
is for backward
compatibility. But if you need to, you can get to ObjectContext
from DbContext
by first dropping down to the
ObjectContext
using the IObjectContextAdapter
, as you’ve seen
previously in this book.
ValidateEntity
is yet another
extensibility point that is available during SaveChanges
. But as you’ve seen in this
chapter, you should be considerate of when your code makes use of
ValidateEntity
or SaveChanges
to insert your logic.
ValidateEntity
by default is
executed on every Added
or Modified
object being tracked. It is a good
replacement for code that you may have put in SaveChanges
where you iterate through each
tracked object and perform some validation logic on it.
A big caveat with the ValidateEntity
method, however, is that it is
executed after DetectChanges
has been
called, so you have to be careful about how you go about setting
properties. You can safely set properties using the DbEntityEntry
, but our preference is to avoid
adding nonvalidation logic into a method that is designated for
performing validations.
The SaveChanges
method is a
good place to execute logic where you want to do something with a group
of objects. For example, you might want to log how many reservations are
added in a particular update. While you do have access to this in the
Validate
Entity
method, this is something you want
to execute only once during a save.
Microsoft’s guidance is to use ValidateEntity
to perform validation logic
(rule checking) only. Their primary reason for this guidance is concern
over incorrectly coded property modifications that won’t get picked up
by the context if the developer is unaware of the fact that DetectChanges
was already called—and will not
be called again. Another is that in ValidateEntity
,
the team has ensured that lazy loading won’t have unexpected effects on
the validation.
From a perspective of architectural guidance, yet another reason
is that by not forcing ValidateEntity
to perform non-validation logic, you follow the principle of Single
Responsibility. ValidateEntity
is for validating. Single
Responsibility helps to keep in mind the fact that if you introduce
other features into that method, you’ll increase the difficulty of
maintaining your application as it grows and evolves.
So far we’ve focused on the entityEntry
parameter of ValidateEntity
. There is also an IDictionary<object, object>
parameter
available:
protected override
DbEntityValidationResult
ValidateEntity (DbEntityEntry
entityEntry,IDictionary
<object
,object
> items)
By default, the value of this parameter is null, but you can use the
parameter to pass additional values to custom implementations of IValidatableObject.Validate
or ValidationAttributes
.
Watch for a change to the IDictionary
parameter in a future version of
Entity Framework: it may be changed to default to an empty dictionary
rather than null. That would make coding against it simpler. As of
Entity Framework 4.3, the parameter is still null.
For example, recall the signature of IValidatableObject.Validate
:
public
IEnumerable
<ValidationResult
> Validate(ValidationContext
validationContext)
ValidationContext
implements
iDictionary
. Entity Framework passes the items
defined in ValidateEntity
to this validationContext
parameter. It’s also possible
to use a ValidationContext
when
creating overrides of the ValidationAttribute
class. (See the MSDN Library documentation topic ValidationAttribute
Class for more information
about this feature: http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.validationattribute.aspx)
You can create a dictionary of objects in the ValidateEntity
method and pass them along in the
base ValidateEntity
call by assigning
the dictionary to the items
variable.
Those objects would then be available for you to use in validations that
accept a ValidationContext
.
For example, you may want to be sure that a newly added or modified
payment does not cause the saved payments to exceed the cost of the trip
on which the reservation is booked. To validate this rule, you would need
the data layer to access the database when it’s validating new or modified
payment objects. But rather than performing all of the calculations in the
data layer, you could have the data layer provide the necessary
information to the payment so that the rule can be included in the
business logic for the Payment
itself.
I’ve seen examples where the DbContext
itself is passed back into the
ValidationContext
of IValidatable.Validate
or ValidationAttribute
s from ValidateEntity
. Neither of us are fans of this
pattern because it forces the object to be aware of the context, of the
data layer, and of Entity Framework. Not only is the class no longer
POCO, but it also removes another quality that I have learned to admire,
persistence ignorance.
Example 7-13 shows
the Validate
method for Payment
after we’ve modified Payment
to implement the IValidatableObject
interface. There are two
validations in the method. The method first checks to see if there are
PaymentSum
and TripCost
items in the validationContext
. The method expects that the
method that has triggered Validate will have created these items in the
dictionary passed in as the validationContext
parameter. If they are there,
the method will use those to compare the payments to the trip cost.
public
IEnumerable
<ValidationResult
> Validate(ValidationContext
validationContext) {var
vc = validationContext;//for book readability
if
(vc.Items.ContainsKey("DbPaymentTotal"
) && vc.Items.ContainsKey("TripCost"
)) {if
(Convert
.ToDecimal(vc.Items["DbPaymentTotal"
]) + Amount >Convert
.ToDecimal(vc.Items["TripCost"
])) {yield return new
ValidationResult
("Oh horrors! The client has overpaid!"
,new
[] {"Reservation"
}); } } }
This example is to demonstrate the use of the IDictionary
and not meant as the de facto
pattern for checking for overpayments in a production application. There
are many more factors to take into consideration for this particular use
case regardless of whether you are using Entity Framework validation or
any other validation pattern.
Given that the Payment
class
validation expects to be provided with a ValidationContext
that supplies the sum of
Payments
for a single Reservation
that are already in the database and
the cost of the trip for which the payment and reservation are made, the
ValidateEntity
method needs to add
those values into the IDictionary
.
Example 7-14 does just
that—retrieving the total of payments for the Reservation
and the TripCost
from the database, and then adding them
to the _items
IDictionary
. This example also places the
particular validation logic, FillPaymentValidationItems
, in a separate method
in order to keep the ValidateEntity
method cleaner.
protected override
DbEntityValidationResult
ValidateEntity (DbEntityEntry
entityEntry,IDictionary
<object
,object
> items) {var
_items =new
Dictionary
<object
,object
>(); FillPaymentValidationItems(entityEntry.Entityas
Payment
, _items);return base
.ValidateEntity(entityEntry, _items); }private void
FillPaymentValidationItems(Payment
payment,Dictionary
<object
,object
> _items) {if
(payment ==null
) {return
; }//calculate payments already in the database
if
(payment.ReservationId > 0) {var
paymentData = Reservations .Where(r => r.ReservationId == payment.ReservationId) .Select(r =>new
{ DbPaymentTotal = r.Payments.Sum(p => p.Amount), TripCost = r.Trip.CostUSD }).FirstOrDefault(); _items.Add("DbPaymentTotal"
, paymentData.DbPaymentTotal); _items.Add("TripCost"
, paymentData.TripCost); } }
Notice that ValidateEntity
doesn’t check the type of the entityEntry.Entity
. Instead it leverages the
performance benefit of casting with as
,
which will return a null
if the entity
is not a Payment
. Then the helper
method does a quick check for a null
before bothering with the inner logic. This is simply a design decision
made on our part.
The method first ensures that the ReservationId
has been set. If the user is
adding a new Reservation and Payment together, then the Reservation
won’t have a Reservation
Id
yet and therefore it won’t have been set
on the Payment.
As we’ve pointed out earlier, by default, Entity Framework will only
send Added
and Modified
entities to the ValidateEntity
method. Internal code checks the
state before calling ValidateEntity
in
a virtual
(Overridable
in VB) method of DbContext
called ShouldValidateEntity
.
After some internal evaluation, ShouldValidateEntity
returns a Boolean
based on this line of code which checks
to see if the state is either Modified
or Added
:
return ((entityEntry.State & (EntityState.Modified | EntityState.Added)) != 0);
Because ShouldValidateEntity
is
virtual, you can override the default logic and specify your own rules for
which entities are validated. You may want only certain types to be
validated or you may want validation performed on Deleted
objects.
As an example, it wouldn’t make sense to delete Reservation
s for Trip
s that are in the past. If you want to
capture deleted Reservation
s in the
data layer and check to make sure they aren’t for past Trip
s, you’ll have to make sure the deleted
Reservation
makes it to the ValidateEntity
method. You don’t have to send
all deleted objects. Instead you can “open up” the pipeline only for
deleted Reservations
.
Add the ShouldValidateEntity
method to the BreakAwayContext
. You can
use the same steps to override the method explained in the earlier note
when adding the ValidateEntity
method.
Example 7-15 shows the
ShouldValidateEntity
method after we’ve
added additional logic to allow deleted Reservation
objects to be validated as well. If
the entity being evaluated is a deleted Reservation
, ShouldValidateEntity
will return true
. If not, it will perform its default logic
to determine whether or not to validate the entity.
protected override bool
ShouldValidateEntity(DbEntityEntry
entityEntry) {return base
.ShouldValidateEntity(entityEntry) || ( entityEntry.State ==EntityState
.Deleted && entityEntry.Entityis
Reservation
); }
Once you’ve allowed the entry to pass through to ValidateEntity
, you’ll need to add logic to
ValidateEntity
to perform the new
validation.