Developers often spend a lot of time writing validation logic in their applications. Many of the rules for validation are built into their classes, but .NET can’t magically verify those rules. Code First allows you to apply some rules directly to properties using Data Annotations or the Fluent API. For example, you can specify the maximum length of a string or the fact that a particular property is required (i.e., can’t be null).
Another type of rule that your model describes is relationship
constraints. For example, in our model, a Lodging
is required to have a related Destination
. Entity Framework has always checked
that relationship constraint rules are met before it will push inserts,
updates, or deletes to the database.
The DbContext
adds to this existing
validation with the new Validation API that is associated with the DbContext
. Using the Validation API, the DbContext
can automatically (or on demand)
validate all of the rules that you have defined using mechanisms that the
validation will recognize. The API takes advantage of features that already
exist in .NET 4—ValidationAttribute
s and
the IValidatableObject
. This integration
is a great benefit to developers. Not only does it mean that you can
leverage existing experience if you’ve worked with the features already, but
it also means that Entity Framework validation can flow into other tools
that use this class or interface.
Validation in the data layer is an important element of data-focused applications. While you may have client-side or business layer validations, you may desire or prefer to have one last bastion of validation before data is pushed into the database. In scenarios where client-side validation is performed in a web application dependent on JavaScript being enabled in a browser, data layer validation plays an important role when the end user has disabled JavaScript.
In this chapter, you’ll learn how to take advantage of the built-in
validation provided by the DbContext
and
Validation API using its default behaviors.
The Validation API checks rules that you can apply in a number of ways:
Property attribute rules that you can specify using Data Annotations or the fluent API.
Custom validation attributes that can be defined for properties or types.
Rules defined in the Validate
method of any model class that implements IValidatableObject
. This interface is part
of .NET 4, so it’s great to see that the DbContext
was designed to take advantage of
this.
Relationship constraints explicitly defined in the model.
Additionally, you can inject validations into the validation pipeline.
There are a number of ways to cause the DbContext
to execute the validations:
By default, validation will be performed on all tracked Added
and Modified
objects when you call SaveChanges
.
The DbEntityEntry.GetValidationResult
method
will perform validation on a single object.
DbEntityEntry
has a path for validating an
individual property.
DbContext
.GetValidationErrors
will iterate through
each Added
and Modified
object being tracked by the
DbContext
and validate each
object.
In the next chapter you’ll learn how to override ValidateEntity
as well as change the default
that validates only Added
or Modified
objects.
At the root of all of these validation methods is the DbEntityEntry.GetValidationResult
method, which
validates the rules defined in property attributes and IValidatableObject
s. GetValidationErrors
calls ValidateEntity
on each [Added
or Modified
] tracked object, which in turn calls
GetValidationResult
. SaveChanges
calls GetValidationErrors
, which means that the
validation occurs automatically whenever SaveChanges
is called. Figure 6-1 shows the waterfall
path and different entry points to leverage the Validation API.
The “it just works” approach of having validation implicitly called
by SaveChanges
may be all that some
developers need or are interested in. But rather than start with the
appearance of magic, we’ll use a bottom-up approach to show you the
explicit validation functionality so that you can use that to have more
control over how and when validation occurs.
Our sample classes already have some attributes that will be checked
by the Validation API. For example, in the Destination
class, you should already have a
MaxLength
annotation on the Description
property shown here:
[MaxLength
(500)]public string
Description {get
;set
; }
Refer to the listing for Destination
shown in Example 2-1.
Testing this rule won’t be very easy since breaking it would mean
adding a string that is greater than 500 characters. I don’t feel like
typing that much. Instead, I’ll add a new MaxLength
annotation to another property—the
LastName
property in the Person
class:
[MaxLength
(10)]public string
LastName {get
;set
; }
Now let’s see what happens when we set the length to a string with
more than ten characters. GetValidationResult
allows you to explicitly
validate a single entity. It returns a ValidationResult
type that contains three
important members. We’ll focus on just one of those for now, the IsValid
property, which is a Boolean that
indicates if the instance passed its validation rules. Let’s use that to
validate a Person
instance. The
ValidateNewPerson
method in Example 6-1 shows calling the
GetValidationResult.IsValid
method.
private static void
ValidateNewPerson() {var
person =new
Person
{ FirstName ="Julie"
, LastName ="Lerman"
, Photo =new
PersonPhoto
{ Photo =new
Byte
[] { 0 } } };using
(var
context =new
BreakAwayContext
()) {if
(context.Entry(person).GetValidationResult().IsValid) {Console
.WriteLine("Person is Valid"
); }else
{Console
.WriteLine("Person is Invalid"
); } } }
If you run this method from the Main
method, you will see the message “Person is
Valid” in the console windows.
The GetValidationResult
method
calls the necessary logic to validate any ValidationAttribute
s defined on the object’s
properties. It then looks to see if the type has a CustomValidationAttribute
or implements the
IValidatableObject
interface and if it
does, calls its Validate
method. You’ll
see this in action later in this chapter.
While we strongly recommend against calling data access code directly in the user interface, these examples are solely for the purpose of demonstrating the Validation API features. We are not suggesting that you use the DbContext for performing client-side validation.
Now change the code so that it sets LastName
to “Lerman-Flynn” instead of Lerman.
Run the app again and you will see “Person is Invalid” in the console. The
Validation API of the DbContext
was
able to detect that a rule was broken. This is just a high-level look at
the method. Let’s explore more ways to define rules before we dig further
into the result of the validation.
The MaxLength
Data Annotation is
exposed via the MaxLengthAttribute
class. MaxLengthAttribute
is one of a
group of attributes that inherit from a class called System.Data.Annotations.ValidationAttribute
.
GetValidationResult
checked MaxLength
because it’s designed to check any
rule that is applied using a ValidationAttribute
.
The Validation API will check any rule that is applied using a
ValidationAttribute
.
Following is a list of the attribute classes that derive from
ValidationAttribute
along with the
annotation used to decorate a class property:
DataTypeAttribute
[DataType(
DataType
enum
)]
RangeAttribute
[Range (
low
value, high value, error message string
)]
RegularExpressionAttribute
[RegularExpression(@”
expression
”)]
RequiredAttribute
[Required]
StringLengthAttribute
[StringLength(
max length
value
,
MinimumLength=
min length
value
)]
CustomValidationAttribute
This attribute can be applied to a type as well as to a property.
For the sake of describing database schema mappings, the Entity
Framework team added MaxLengthAttribute
to the namespace and paired it with a new MinLengthAttribute
. They both derive from
ValidationAttribute
, so these too will
be checked by the Validation API.
Both MaxLength
and Required
are not just ways to define a class
property but they are also used to describe a database column to which the
properties map. Technically, these are referred to as facets. Therefore,
in Entity Framework, these two facets play dual rules—they help Code First
understand what the mapped database columns look like and they also
participate in the class-level validation. An added benefit is that since
you can also define these two facets—MaxLength
and Required
—with the Fluent API, Entity Framework
will take advantage of the relevant ValidationAttribute
types under the covers to
make sure they get validated as if they had been configured with the Data
Annotations.
Entity Framework has been taught to look for the StringLength
annotation and use its MaximumLength
parameter as a database column
facet as well.
If you have used the Fluent API to configure your Code First model, you may be familiar with specifying attributes fluently instead of using Data Annotations.
The Programming Entity Framework: Code First book covers fluent API in detail.
Two of the Data Annotations that inherit from ValidationAttribute
—MaxLength
and Required
—have Fluent API counterparts. This is
due to the fact that MaxLength
and
Required
are attributes that impact
the model’s comprehension of the database schema and therefore impact
how Entity Framework maps the classes to the database.
The Validation API will check these two rules if you configure
them with the Fluent API. For example, you could replace the [MaxLength]
annotation on Person.LastName
with this code added into the
BreakAwayContext.OnModelCreating
method:
modelBuilder.Entity<Person
>()
.Property(p => p.LastName).HasMaxLength(10)
If you return to the ValidateNewPerson
method from Example 6-1, ensure the LastName
property is set to “Lerman-Flynn,”
and then rerun the method, it will result in a message from the console
application: “Person is Invalid.”
Underneath the covers, Entity Framework is using the StringLengthAttribute
(or in the case of a
Required
scalar, the RequiredAttribute
) to validate the HasMaxLength
facet of Person.FirstName
. Although the example only
checks the IsValid
property, the
details of the error are returned by GetValidationResult
, and you’ll see how to
read these shortly.
It is possible to have properties in your class that do not map to
the database. By convention, properties that do not have both a setter
and a getter will not be part of the model. These are also known as
transient properties. You can also configure a
property to be unmapped using the NotMapped
data annotation or the
Ignore
fluent method. By default, unmapped properties
will not get validated.
However, if you have applied a ValidationAttribute
to a transient property
(as long as that property is in a class that is part of the model),
Entity Framework will validate those rules as well.
Entity Framework’s conceptual model supports the use of complex
types, also known as value objects. You can configure a complex type
both in the Entity Data Model designer as well as with Code First. It is
also possible (and feasible) to apply attributes to the properties of
complex types. Entity Framework’s GetValidationResult
will validate attributes
placed on complex type properties.
It’s easy to apply data annotations to your class and then use that class with Entity Framework thanks to Code First, but what if you are using the Entity Data Model designer to create your Database First or Code First model and then relying on code generation to create your classes? There’s no opportunity to apply Data Annotations to your properties. You might modify the T4 template to apply Data Annotations that follow very common patterns in your classes, but typically this is not the appropriate mechanism for applying property-by-property attributes.
The generated classes are partial classes, which does give you the ability to add more logic to the classes with additional partial classes. However, you cannot add attributes in one partial class to properties that are declared in another partial class.
But all is not lost. There is a feature in .NET called an associated metadata class that allows you to add metadata to classes in an external file. These classes are commonly referred to as “buddy classes,” although we are more fond of the term “ugly buddy classes” because they feel a little kludgy. However ugly, they are a great way to apply data annotations to generated code. So setting aside illusions of grandeur about our code, let’s take a look at a simple example of an associated metadata class.
David Ebbo has an interesting blog post on other ways to use the
Metadata
attribute: http://blogs.msdn.com/b/davidebb/archive/2009/07/24/using-an-associated-metadata-class-outside-dynamic-data.aspx
If you are using the DbContext Generator template to generate
classes from an EDMX, the Person
class will be declared as a partial class:
public partial class
Person
and the scalar properties will be simple. Here is what the
FirstName
property will look
like:
public string
FirstName {get
;set
; }
You can create a new class where you can mimic the property declaration and apply the attribute:
class
Person_Metadata
{ [MinLength
(10)]public string
FirstName {get
;set
; } }
Then you need to let the Person
class know to use the Person_Metadata
class for the sole purpose of reading the attributes.
You do this by applying the Metadata
attribute to the Person
class:
[MetadataType
(typeof
(Person_Metadata
))]public partial class
Person
If you want to try this out in the Code First sample you’ve been
working with, be sure to remove or comment out the
MinLength
annotation on the
FirstName
property in the Person
class.
Notice that GetValidationResult
doesn’t simply throw an exception if the validation fails. Instead, it
returns a System.Data.Entity.Validation.DbEntityValidationResult
whether the rule is met or broken, setting IsValid
to the appropriate value and providing
detailed information on any broken rules.
DbEntityValidationResult
also
exposes a ValidationErrors
property,
which contains a collection of more detailed errors in the form of
DbValidationError
types. One final
property of DbEntityValidationResult
is a pointer. In
this scenario, it seems redundant to have the Entry
property when we started with the Entry
to get the results. However, when one of
the higher-level methods calls GetValidationResult
on your behalf, you may not
know which Entry
is currently being
validated; in that scenario, you’ll probably be grateful for the Entry
property.
Figure 6-2 shows
getting to the Entry
, IsValid
, and ValidationErrors
properties in the
debugger.
Looking back at Figure 6-2, you’ll see that
when we set the LastName
to Lerman-Flynn
, which exceeded the MaxLength(10)
specification, the result’s
ValidationErrors
collection contains
a single DbValidationError
. DbValidationError
exposes two properties, the
name of the property and the actual error message.
Where did the error message come from? The internal validation logic has a formula that composes a message using the property name and the annotation that failed. This is default behavior.
You can specify your own error message in any ValidationAttribute
. For example, if you were
writing error messages for an application used by surfers, you might
want to specify one like this:
[MaxLength
(10, ErrorMessage="Dude! Last name is too long! 10 is max."
)]public string
LastName {get
;set
; }
Figure 6-3 shows
the new ErrorMessage
returned in a
DbEntityValidationError
.
Because the ValidationAttribute
type is part of .NET 4
and not specific to Entity Framework, we won’t spend a lot of time
going into great detail about how to configure the ValidationAttribute
types. Other .NET
frameworks, such as Managed Extensibility Framework (MEF), ASP.NET
MVC, and ASP.NET Dynamic Data, use this functionality, and there is a
lot of information available. To start, here is the MSDN Topic on the
ValidationAttribute
class: http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.ValidationAttribute.aspx.
As mentioned in the earlier note, MEF, MVC, and Dynamic Data are
able to leverage the ValidationAttribute
. Although the ValidateNewPerson
method demonstrated using
DbContext
to perform the validation,
it is also possible to validate ValidationAttributes
directly using its
Validate method. This is a great benefit for client-side development.
However, since the focus of this book is DbContext
, we’ll focus on how the DbContext
works with the ValidationAttributes
at the data layer.
Remember that DbContext
checks more
than just ValidationAttributes
, so
you can benefit from these as well as other rules all at once on the
server side with DbContext
.
If you are calling GetValidationResults
directly, you will have
to write your own logic to interact with the DbEntityValidationResult
, read the errors, and
handle them, whether for logging or returning to the UI to present to
the user.
In the case of the validation detected in ValidateNewPerson
, there is a single error
inside the ValidationErrors
property. You could get
to it by requesting that first error. For example:
var
result = context.Entry(person).GetValidationResult();if
(!result.IsValid) {Console
.WriteLine( result.ValidationErrors.First().ErrorMessage); }
That will work if you only expect or only care about the first
error. However, if you have numerous ValidationAttributes
defined on a type and
more than one is broken, there will be more than one DbEntityValidationError
in the Validation
Errors
property.
What if the Person
class also
had a rule that the FirstName
property must be at least three characters?
[MinLength
(3)]public string
FirstName {get
;set
; }
If you were to update the ValidateNewPerson
method from Example 6-1 to insert just the
letter J as the FirstName
, the validation will see
two errors, as shown in Figure 6-4.
It might be wiser, therefore, to iterate through the errors.
You’ll need to add a using statement for the
System.Data.Entity.Validation
namespace:
foreach
(DbValidationError
errorin
result.ValidationErrors) {Console
.WriteLine(error.ErrorMessage); }
So far we’ve looked at the MaxLength
property, which is not only a ValidationAttribute
, but is an attribute that’s
in the EntityFramework assembly.
Let’s look at an attribute that is not specific to Entity Framework, the
RegularExpressionAttribute
, and verify
that the DbContext.GetValidationResult
will see that as well.
Following is a RegularExpression
applied to the Destination.Country
property. This expression specifies that the string can be up to 40
characters and will accept uppercase and lowercase letters:
[RegularExpression
(@"^[a-zA-Z''-'s]{1,40}$"
)]public string
Country {get
;set
; }
The ValidateDestinationRegEx
method in Example 6-2 creates a
new Destination
and asks the DbContext
to validate the instance.
You’ll notice that we’ve also refactored the method to move the
call to GetValidationResult
and
Console.WriteLine
into a separate
method called ConsoleValidationResults
. See the sidebar
Simplifying the Console Test Methods to see this new
method.
public static void
ValidateDestination() { ConsoleValidationResults(new
Destination
{ Name ="New York City"
, Country ="USA"
, Description ="Big city"
}); }
With the Country
set to “USA,”
the property is valid and no errors are displayed. However, if you change
the Country
value to “U.S.A.,” GetValidationResult
detects an error because the
periods in between the letters do not follow the rule defined by the
regular expression. Be aware that the default error message only reports
that the value does not match the expression; it does not tell you which
part of the expression was broken:
The field Country must match the regular expression '^[a-zA-Z''-'s]{1,40}$'.
This is not a problem with how Entity Framework handles the
validation. It is simply how the RegularExpressionAttribute
behaves by default.
You can learn more about controlling this error message in the MSDN
documentation referenced above.
You can build custom validation logic that can be applied to a
property using a CustomValidationAttribute
. These too will get
checked during Entity Framework validation. Example 6-3 shows an example of
a static class, BusinessValidations
,
which contains a single validation, DescriptionRules
, to be used on various
description properties in the model. The rule checks for exclamation
points and a few emoticons to ensure that trip descriptions or other
descriptions don’t read as though they were text messages! ☺
using
System.ComponentModel.DataAnnotations;namespace
Model {public static class
BusinessValidations
{public static
ValidationResult
DescriptionRules(string
value) {var
errors =new
System.Text.StringBuilder
();if
(value !=null
) {var
description = valueas string
;if
(description.Contains("!"
)) { errors.AppendLine("Description should not contain '!'."
); }if
(description.Contains(":)"
) || description.Contains(":("
)) { errors.AppendLine("Description should not contain emoticons."
); } }if
(errors.Length > 0)return new
ValidationResult
(errors.ToString());else
return
ValidationResult
.Success; } } }
The ValidationResult
used
here is a System.ComponentModel.DataAnnotations.ValidationResult
,
not to be confused with the System.Windows.Controls.ValidationResult
.
You can apply the validation to properties using the CustomValidationAttribute
, as shown in Example 6-4, where we’ve added
the annotation to the Destination.Description
property (which
already has a MaxLength
annotation).
The attribute requires that you specify the class where the validation
method exists and then the name of the method as a string.
[MaxLength
(500)] [CustomValidation
(typeof
(BusinessValidations
),"DescriptionRules"
)]public string
Description {get
;set
; }
If you’d like to test out the validation, you can modify the
ValidateDestination
method to insert
some of the undesirable characters into the Description
string, as we’ve done in Example 6-5.
public static void
ValidateDestination() { ConsoleValidationResults(new
Destination { Name ="New York City"
, Country ="U.S.A"
, Description ="Big city! :) "
}); }
Executing the method will cause the validation to return the following list of errors:
The field Country must match the regular expression '^[a-zA-Z''-'s]{1,40}$'. Description should not contain '!'. Description should not contain emoticons.
Both the RegularExpression
validation on the Country
property
and the DescriptionRules
custom
validation on Description
are
reported.
In addition to providing the GetValidationResults
method, DbEntityEntry
lets you drill into individual
properties, as you’ve already seen in Chapter 5:
context.Entry
(trip).Property
(t => t.Description
);
This returns a DbPropertyEntry
representing the Description
property.
The DbPropertyEntry
class has a
method for explicitly validating that particular entry—GetValidationErrors
—which will return an
ICollection<DbValidationError>
.
This is the same DbValidationError
class we’ve been exploring already in this chapter.
Example 6-6 displays a new method,
ValidatePropertyOnDemand
, which shows
how to validate a property using DbPropertyEntry.GetValidationErrors
. You’ll
first need to apply the DescriptionRules
custom attribute to the
Trip.Description
property, just as you
did for Destination.Description
in
Example 6-4.
private static void
ValidatePropertyOnDemand
() {var
trip=new
Trip
{EndDate
=DateTime
.Now
,StartDate
=DateTime
.Now
,CostUSD
= 500.00M,Description
="Hope you won't be freezing :)"
};using
(var
context =new
BreakAwayContext
()) {var
errors = context.Entry
(trip) .Property
(t => t.Description
) .GetValidationErrors
();Console
.WriteLine
("# Errors from Description validation: {0}"
, errors.Count
()); } }
The method creates a new Trip
that has an emoticon in the Description
. Based on the custom validation rule
you created earlier in this chapter, the emoticon is invalid.
If you were to call this method from the Main
method in the console application, the
console window would report that there is one error in the Description
. Keep this method around, because
we’ll look at it again in the next section.
While there are more ways to trigger validations, let’s stick with
the GetValidationResult
method while we
look at other ways to provide rules that the Validation API will validate.
So far you’ve seen how to apply validation rules on individual properties.
You can also define rules for a type that can take multiple properties
into account. Two ways to create type-level validation that will be
checked by the Entity Framework Validation API are by having your type
implement the IValidatableObject
interface or defining CustomValidationAttribute
s for type. This
section will explore both of these options.
In addition to the ValidationAttribute
, .NET 4 introduced another
feature to help developers with validation logic—the IValidatableObject
interface. IValidatableObject
provides a Validate
method to let developers (or
frameworks) provide their own context from which to perform the
validation.
If an entity that is being validated implements the IValidatableObject
interface, the Validation
API logic will recognize this, call the Validate method, and surface the
results of the validation in a DbEntityValidationError
.
What does IValidatableObject
provide that is not satisfied with Data Annotations? The Data
Annotations let you specify a limited number of rules for individual
properties. With the additional Validate
method, you can provide any type of
logic that can be constrained to the class. What we mean by constrained
is that the validation logic won’t rely on external objects since you
can’t guarantee that they’ll be available when the validation is being
performed. A typical example is comparing date properties in a
class.
The Trip
type has StartDate
and EndDate
fields. Let’s use IValidatableObject
to define a rule that
EndDate
must be greater than StartDate
.
Example 6-7 shows
the Trip
class after we’ve added the
IValidatableObject
implementation
that includes the Validate
method.
Validate
compares the dates and returns a ValidationResult
if the rule is broken. Notice
that we’ve also added the DescriptionRules
attribute we created in Example 6-3 to the Description
field.
public class
Trip : IValidatableObject
{ [Key, DatabaseGenerated(DatabaseGeneratedOption
.Identity)]public
Guid
Identifier {get; set;
}public
DateTime
StartDate {get; set;
}public
DateTime
EndDate {get; set;
} [CustomValidation
(typeof
(BusinessValidations
),"DescriptionRules"
)]public string
Description {get; set;
}public decimal
CostUSD {get; set;
} [Timestamp
]public byte
[] RowVersion {get; set;
}public int
DestinationId {get; set;
} [Required
]public
Destination
Destination {get; set;
}public
List
<Activity
> Activities {get; set;
}public
IEnumerable
<ValidationResult
> Validate(ValidationContext
validationContext) {if
(StartDate.Date >= EndDate.Date) {yield return new
ValidationResult
("Start Date must be earlier than End Date"
,new
[] {"StartDate"
,"EndDate"
}); } } }
Visual Basic (VB) does not have a yield keyword. Instead, you can
create a List<ValidationResult>
, add each
ValidationResult
into that list, and
then return it. Example 6-8 shows the Validate
method as you would write it in
VB.
Public Function
Validate(ByVal
validationContextAs
ValidationContext
)As
IEnumerable
(Of
ValidationResult
)Implements
IValidatableObject
.ValidateDim
results =New
List
(Of
ValidationResult
)If
StartDate.Date >= EndDate.DateThen
results.Add(New
ValidationResult
("Start Date must be earlier than End Date"
, {"StartDate"
,"EndDate"
}))End If
Return
resultEnd Function
We’ve added a new method to the console application called
ValidateTrip
, shown in Example 6-9.
private static void
ValidateTrip() { ConsoleValidationResults(new
Trip
{ EndDate =DateTime
.Now, StartDate =DateTime
.Now.AddDays(2), CostUSD = 500.00M, Destination =new
Destination
{ Name ="Somewhere Fun"
} }); }
When calling ValidateTrip
, the
application displays the error message, “Start Date must be earlier than
End Date.” But it’s listed twice. That’s because the Validate method
listed this as a problem for both StartDate
and EndDate
, so it created two separate errors.
The DbValidationError.ErrorMessage
is
the same in both, but one has “EndDate” in its DbValidationError.PropertyName
while the other
has “StartDate.”
This is important for data binding with frameworks such as MVC or
WPF where you can bind the errors to the displayed properties. If we
modify the ValidateTrip
method to
ensure that EndDate
is a later date than
StartDate
, the ValidateTrip
method returns no error
messages.
Mapped complex types that implement IValidatableObject
will be checked in the
validation pipeline as well.
You can add as many class validations as you like in your Validate
method. With the C# yield keyword,
all of the ValidationResult
types
created will be contained within the IEnumerable
that’s returned by the
method.
Example 6-10 shows the
Trip.Validate
method with a second
validation added that checks against a list of words that are
undesirable for describing trips. You could use a RegularExpression
annotation with the word
list, but this method gives you the opportunity to store the list of
words in a resource file so that it’s not hard-coded into the
application. The list is hard-coded into this example only for the
simplicity of demonstrating the validation. You’ll need to add a using
statement for the System.Linq
namespace.
public
IEnumerable
<ValidationResult
> Validate(ValidationContext
validationContext) {if
(StartDate.Date >= EndDate.Date) {yield return new
ValidationResult
("Start Date must be earlier than End Date"
,new
[] {"StartDate"
,"EndDate"
}); } var unwantedWords =new
List
<string
> {"sad"
,"worry"
,"freezing"
,"cold"
};var
badwords = unwantedWords .Where(word => Description.Contains(word));if
(badwords.Any()) {yield return new
ValidationResult
("Description has bad words: "
+string
.Join(";"
, badwords),new
[] {"Description"
}); } }
Now we’ll modify the ValidateTrip
method to add a Description
(which includes the undesirable
words “freezing” and “worry”) to the new trip before the validation is
performed (Example 6-11).
private static void
ValidateTrip() { ConsoleValidationResults(new
Trip
{ EndDate =DateTime
.Now, StartDate =DateTime
.Now.AddDays(2), CostUSD = 500.00M, Description="Don't worry about freezing on this trip"
, Destination =new
Destination
{ Name ="Somewhere Fun"
} }); }
When running ValidateTrip
with
this trip that now breaks two rules, both error messages are displayed
in the console window:
Start Date must be earlier than End Date Start Date must be earlier than End Date Description has bad words: worry;freezing
In the previous section, Validating Individual Properties on Demand, you created a
method to explore the DbPropertyEntry.GetValidationErrors
. Looking
back at Example 6-6, notice that in
addition to the emoticon in the description, there is what you now know
to be an undesirable word—freezing
.
If you were to run the method again, the console window would still only
report a single error, which is a result of the emoticon. It seems to
ignore the problem with the word freezing
. That’s because the validation that
checks for the word freezing
is
defined for the class. DbPropertyEntry.GetValidationErrors
can only
check ValidationAttribute
s placed on
properties.
You can also use CustomValidationAttribute
on a type rather
than an individual property, allowing you to define a validation that
takes into account more than a single property. We’ll show you how you
can define the same validation in the IValidatableObject
example above by using
CustomValidationAttribute
.
The signature of a CustomValidationAttribute
has the target type
specified in the parameter along with a ValidationContext
, which is used in the same
way as the ValidationContext
parameter of the Validate
method.
Example 6-12 shows two
validation methods that are added into the Trip
class. Notice that these methods are both
public
and static
. Also notice that we’re using separate
methods for each validation. That’s because a ValidationAttribute
can only return a single
ValidationResult
.
public static
ValidationResult
TripDateValidator(Trip
trip,ValidationContext
validationContext) {if
(trip.StartDate.Date >= trip.EndDate.Date) {return new
ValidationResult
("Start Date must be earlier than End Date"
,new
[] {"StartDate"
,"EndDate"
}); }return
ValidationResult
.Success; }public static
ValidationResult
TripCostInDescriptionValidator(Trip
trip,ValidationContext
validationContext) {if
(trip.CostUSD > 0) {if
(trip.Description .Contains(Convert
.ToInt32(trip.CostUSD).ToString())) {return new
ValidationResult
("Description cannot contain trip cost"
,new
[] {"Description"
}); } }return
ValidationResult
.Success; }
The first method, TripDateValidator
, mimics a validation you
used earlier—checking that the StartDate
is earlier than the EndDate
. The second method, TripCostInDescriptionValidator
, checks to make
sure that a user hasn’t written the trip cost into the description. The
logic in that method could be fine-tuned for a production application,
but it should suffice for this demonstration.
There’s a notable difference with these methods when comparing
them to the Validate
method you saw
earlier. The Validate method has access to private methods, properties,
and fields. But because of the way the ValidationAttribute
is handled under the
covers triggering those methods (which is also why they must be public
and static
), it will not have this access.
To have both validations executed, you need to add them as separate attributes on the Trip class, as shown in Example 6-13.
[CustomValidation
(typeof
(Trip
),"TripDateValidator"
)] [CustomValidation
(typeof
(Trip
),"TripCostInDescriptionValidator"
)]public class
Trip: IValidatableObject
If you were to run the ValidateTrip
method, the console window would
display this:
Start Date must be earlier than End Date Start Date must be earlier than End Date Start Date must be earlier than End Date Start Date must be earlier than End Date Description has bad words: worry;freezing
As a reminder, because the validator creates the ValidationResult
specifying that it’s for both
the StartDate
field and for the
EndDate
property, the error is listed
once for each property. If you were to inspect the ValidationResult
more closely, you would see
that the errors are differentiated by their PropertyName
. You’re also seeing the errors
generated by the Validate
method. The
Validate
method is also checking the date range as
well as checking the Description
for unwanted
words.
Modify the ValidateTrip
method
to break the TripCostInDescriptionValidator
rule as
follows, changing the value of the Description
:
private static void
ValidateTrip() { ConsoleValidationResults(new
Trip
{ EndDate =DateTime
.Now, StartDate =DateTime
.Now.AddDays(2), CostUSD = 500.00M, Description ="You should enjoy this 500 dollar trip"
, Destination =new
Destination
{ Name ="Somewhere Fun"
} }); }
Running the application again would result in the two errors from
the problem with the date properties as well as the error message from
the failed TripCostInDescriptionValidator
validation:
Description cannot contain trip cost Start Date must be earlier than End Date Start Date must be earlier than End Date Start Date must be earlier than End Date Start Date must be earlier than End Date
Remember the DescriptionRules
method that we added to the BusinessValidations
class to validate the
Description
property of Destination
and Trip
? Those contain overall rules for writing
any description for the company, not just those for destinations.
Now we’ll modify the Description
in the ValidateTrip
method to include
the dreaded smiley face emoticon and exclamation point:
Description="Hope you won't be freezing on this trip! :)"
Before running the ValidateTrip
method again, keep in mind that the values of this Trip
instance break four rules:
The StartDate
is not at least
a full calendar day before the EndDate
.
The word freezing
is in the
description.
There is an emoticon in the Description
.
There is an exclamation point in the Description
.
Here is the list of messages returned by the method:
Description should not contain '!'. Description should not contain emoticons.
The problems about the dates and the word freezing
are missing from the messages.
To be sure, let’s revert the Description
, removing the exclamation and
emoticon so that it passes the DescriptionRules
but fails the others. This
brings us back to the date problem listed for both the date fields and the
message about the word freezing
:
Start Date must be earlier than End Date Start Date must be earlier than End Date Start Date must be earlier than End Date Start Date must be earlier than End Date Description has bad words: freezing
While this looks like there’s a problem with the validation, the validation is indeed working as designed. We’ve defined both property validation and type validation. The property validations check for the emoticons and exclamation point, while the type validations check the dates and look for bad words. The failure of a property validation is short-circuiting the type validations. In other words, the type validation is never performed because problems were found when validating the properties.
Let’s update the ValidateTrip
method so that it
no longer supplied a value for the Destination
property—which is marked with a Required
attribute:
// Destination = new Destination { Name = "Somewhere Fun" }
If you rerun the ValidateTrip
method, which no longer provides a value for the required
Destination
property, the only error message is
this:
The Destination field is required.
The Required
validation failure
is reported, but the type validations are still missing, so the failure of
this validation also prevented the type validation. If we added the
“! :)
” back into the Description
, you’d see all of the property
validation problems listed (Required, “!”, and the emoticon) but still no
report of the type validation problems.
What’s happening is that there are rules that the validation engine follows that prevent it from erroneously reporting errors that might be caused by other validation errors. If the property validation fails, it’s possible that the bad attributes might cause the type validation to fail as well.
Borrowing from the Entity Framework team blog post at http://blogs.msdn.com/b/adonet/archive/2011/05/27/ef-4-1-validation.aspx, here is a description of the order in which validations are performed:
Property-level validation on the entity and the base classes. If a property is complex its validation would also include the following:
Property-level validation on the complex type and its base types
Type-level validation on the complex type and its base types,
including IValidatableObject
validation on the complex type
Type-level validation on the entity and the base entity types,
including IValidatableObject
validation
The key point is that type-level validation will not be run if
property validation returns an error. In addition to the IValidatableObject
validations, relationship
constraints are validated. Since it’s possible that one of the property
failures was due to a missing required property, that null value could
very easily cause a relationship to be invalid. The Validation API does
not allow constraint checking to occur if the properties cannot be
validated.
In addition to explicitly validating a single object with GetValidationResult
, you can force the context
to validate all of the necessary objects it is
tracking with a single command: DbContext.GetValidationErrors
. I’ve emphasized
the word “necessary” because, by default, GetValidationErrors
only validates Added
and Modified
objects since it typically wouldn’t be
necessary to validate objects that are Unchanged
or are marked to be deleted from the
database.
When you call this method, the context will internally call DetectChanges
to ensure that all of the change
tracking information is up-to-date. Then it will iterate through all of
the Added
and Modified
objects that it’s tracking and call
DbContext
.ValidateEntity
on each object. ValidateEntity
, in turn, will call GetValidationResult
on the target object. When
all of the objects have been validated, GetValidationErrors
returns a collection of
DbEntityValidationResult
types for
every failed object. The collection is returned as an IEnumerable< DbEntityValidationResult >
and it only contains DbEntityValidationResult
instances for the
failed objects.
In addition to calling GetValidationResult
, ValidateEntity
can also call custom logic that
you specify. The next chapter will focus on customizing ValidateEntity
. You’ll also learn to modify
how Entity Framework decides which entities to validate by overriding
the default, which only validates Added
and Modified
entities.
Example 6-14 displays a method that results in a context tracking
two new Trip
s,
one new Destination
,
and one modified Trip
.
If you look closely at the code, you’ll see that one of the new trips will be valid while the other three objects are not valid.
private static void
ValidateEverything() {using
(var
context =new
BreakAwayContext
()) {var
station =new
Destination
{ Name ="Antartica Research Station"
, Country ="Antartica"
, Description ="You will be freezing!"
}; context.Destinations.Add(station); context.Trips.Add(new
Trip
{ EndDate =new
DateTime
(2012, 4, 7), StartDate =new
DateTime
(2012, 4, 1), CostUSD = 500.00M, Description ="A valid trip."
, Destination = station }); context.Trips.Add(new
Trip
{ EndDate =new
DateTime
(2012, 4, 7), StartDate =new
DateTime
(2012, 4, 15), CostUSD = 500.00M, Description ="There were sad deaths last time."
, Destination = station });var
dbTrip = context.Trips.First(); dbTrip.Destination = station; dbTrip.Description ="don't worry, this one's from the database"
; DisplayErrors(context.GetValidationErrors()); } }
Along with the ValidateEverything
method in Example 6-14, add the
DisplayErrors
custom method (Example 6-15) to the Program
class. This will iterate through the
DbEntityValidationResult
objects
returned by the GetValidationErrors
method and display them in a console window.
private static void
DisplayErrors(IEnumerable
<DbEntityValidationResult
> results) {int
counter = 0;foreach
(DbEntityValidationResult
result in results) { counter++;Console
.WriteLine("Failed Object #{0}: Type is {1}"
, counter, result.Entry.Entity.GetType().Name);Console
.WriteLine(" Number of Problems: {0}"
, result.ValidationErrors.Count);foreach
(DbValidationError
error in result.ValidationErrors) {Console
.WriteLine(" - {0}"
, error.ErrorMessage); } } }
Modify the Main
method to call
ValidateEverything
, which will execute
and display the validation results, as shown in Example 6-16.
Failed Object #1: Type is Destination Number of Problems: 1 - Description should not contain '!'. Failed Object #2: Type is Trip Number of Problems: 5 - Start Date must be earlier than End Date - Start Date must be earlier than End Date - Start Date must be earlier than End Date - Start Date must be earlier than End Date - Description has bad words: sad Failed Object #3: Type is Trip Number of Problems: 1 - Description has bad words: worry
GetValidationErrors
does not
check relationship constraints unless they are explicitly configured. For
example, by default, the Reservation.Traveler
property is nullable. There
are two ways to force the Reservation
to require that a Person
type be
attached. One is to add an int TravelerId
property and configure that to be the
foreign key for Traveler
. Int is
non-nullable by default. ValidateEntity
will not check that constraint and therefore GetValidationErrors
won’t either.
SaveChanges
will detect
relationship constraint problems even if they are not defined in a way
that ValidateEntity
will trap
them.
Another way to require that a Person
be attached
is to configure the Traveler
property as required. With
a ValidationAttribute
(even if you’ve configured with
the Fluent API), ValidateEntity
will check this rule
and GetValidationErrors
will detect the problem.
In Chapter 3,
you learned about DetectChanges
, the
events that call it by default, and how to disable automatic change
detection. If you have disabled change detection, that means GetValidationErrors
won’t call it either and
you should make an explicit call to DetectChanges
before calling GetValidationErrors
.
While you may prefer to have explicit control over when GetValidationResults
is called, Entity Framework
can automatically perform the validations when you call SaveChanges
. By default, when you call SaveChanges
, each Added
and Modified
entity that is being tracked by the
context will be validated because SaveChanges
calls Get
Validation
Errors
.
Later in this section, you’ll learn how to disable the automatic
validation that occurs during SaveChanges
. You may already be familiar with
how ObjectContext.SaveChanges
works
in the Entity Framework. For a brief overview, it follows this workflow
(note that this is not taking DbContext
into account yet —only the internal
workflow):
SaveChanges
, by default,
calls DetectChanges
to update its
tracking information on POCO objects.
SaveChanges
iterates
through each tracked entity that requires some modification (those
with states Added
, Modified
, or Deleted
).
For each of these entities, it checks that their relationship
constraints are in a proper state. If not, it will throw an EntityUpdateException
for that entity and
stop further processing.
If all of the entities pass the relationship validation, EF constructs and executes the necessary SQL command(s) to perform the correct action in the database.
If the database command fails, the context responds by
throwing an EntityUpdateException
and stops further processing.
Because SaveChanges
uses a
DbTransaction
by default, in either
of the circumstances that causes the routine to throw an exception, any
of the commands that succeeded up until that point are rolled
back.
When you use DbContext
to call
SaveChanges
, one additional step is
performed prior to the first step in the
ObjectContext.Savechanges
workflow. DbContext.SaveChanges
calls GetValidationErrors
, which runs through the
ValidateEntity
process. If no errors
are found, it then calls ObjectContext.SaveChanges
. Because GetValidationErrors
has already called
DetectChanges
, ObjectContext.SaveChanges
skips its own call
to DetectChanges
.
Figure 6-5 shows
the execution path when your code calls DbContext.SaveChanges
.
What this means to you is that, by default, Entity Framework will
validate all of the rules specified with ValidationAttributes
and IValidatableObject
automatically when you call
SaveChanges
from a DbContext
.
If errors are detected during GetValidationErrors
, SaveChanges
will throw a DbEntityValidationException
with the results
of GetValidationErrors
in its
EntityValidationErrors
property. In
this case, the context will never make the call to ObjectContext.SaveChanges
.
In the previous section, you learned that ValidationEntity
, called by GetValidationErrors
, will not check
relationship constraints unless they are specified in configuration.
However, ObjectContext.SaveChanges
has always checked relationship constraints and continues to do so.
Therefore, any relationship constraints that were not validated by
GetValidationErrors
will be checked
in the next stage of the save. The same applies to null complex
properties. Since null complex properties are not supported, ObjectContext
always checks if a complex
property is not null. Having the Required
attribute on a complex property makes
sense only for consistency reasons (that is, a null complex property
violation will be reported the same way as other validation
violations).
If you’d like to see this in action, you can modify the ValidateEverything
method so that rather than
explicitly calling GetValidationErrors
, it will call SaveChanges
. Replace the final line of the
ValidateEverything
method from Example 6-14 (i.e., the code line
that calls into DisplayErrors)
with
the code in Example 6-17.
You’ll call SaveChanges
instead and
then display information about any validation exceptions.
try
{ context.SaveChanges();Console
.WriteLine("Save Succeeded."
); }catch
(DbEntityValidationException
ex) {Console
.WriteLine("Validation failed for {0} objects"
, ex.EntityValidationErrors.Count()); }
Because this example contains intentional problems that will be
detected during the internal call to GetValidationErrors
, ObjectContext.SaveChanges
will never be
executed and your data will not be persisted to the database. If the
validations were to pass, there’s still a chance of an UpdateException
when the lower-level ObjectContext.SaveChanges
is called
internally, but we’re ignoring that possibility in this example.
If you were to run this method, you would find is that a DbEntityValidationException
is thrown.
DbEntityValidationException
has a
property called EntityValidationErrors
which returns an
IEnumerable
of something you are
already familiar with—EntityValidationResult
s that were created for
each failed entity.
Figure 6-6 shows the DbValidationException
in the debug window
(with private fields removed for clarity).
The exception handler in Example 6-17 displays how many
EntityValidationResult
instances are
contained in the exception, in other words, how many entities failed
validation when SaveChanges
was
called.
Because you already know how to iterate through EntityValidationResult
objects, you can dig
further into the exception if you want to relay the details of the
validation errors.
You may want to exert more control over when validation occurs by
calling the various validation methods explicitly in your application.
You can prevent Entity Framework from triggering the validation during
SaveChanges
thanks to the DbContext.Configuration
property. One of the
settings you can configure on DbContext
is ValidateOnSaveEnabled
. This is set to true by
an internal method when you instantiate a new DbContext
, which means that it’s true by
default on any DbContext
class.
You can disable it in the constructor of your context class so that it’s always false whenever you instantiate a new instance of the context.
For example, in BreakAwayContext
you could add the following
constructor:
public class
BreakAwayContext
:DbContext
{public
BreakAwayContext() { Configuration.ValidateOnSaveEnabled =false
; } ... rest of class logic }
You can also enable or disable this feature as needed throughout your application by modifying the configuration setting on your context instance.
One benefit of disabling the validation on SaveChanges
and calling the validation methods
explicitly is that it allows you to avoid having an exception thrown.
When validations fail inside of the SaveChanges
call, SaveChanges
throws the DbEntityValidationException
. However, as
you’ve seen through this chapter, calling GetValidationResult
or GetValidationErrors
explicitly returns
something whether the validations pass or fail.
GetValidationResult
returns a ValidationResult
that will indicate whether or
not the validation passed. GetValidationErrors
returns an IEnumerable
of ValidationResults
for failed validations and
if there were none, the IEnumerable
will be empty. When application performance is an important factor in
your development process, the expense of exceptions might be the
deciding factor for choosing the automatic validation during SaveChanges
or disabling that and taking
control over how and when validation occurs. You’ll learn more about
taking advantage of this configuration in the next chapter.