An often overlooked aspect of most EF Core based applications is the Entity Data Validation. In my opinion, it is because most tutorials never mention this very important feature to developing secure, robust, stable and essential component of your data layer.
The vast majority of tutorials and documentation I've seen dealing with EF Core and how to implement tend to only focus on the general aspects of the Unit of Work and repository layers and dependency injection and basic queries etc. If tutorials and blog posts, mention Entity Validation at all they tend to focus on the Attribute based implementation, which I am not a fan of, and in general I always advise against using the Attribute based approach with Entity Framework core in general, preferring to make use of Fluent API Configuration for my Entity Framework Core projects, this also the approach Microsoft in general recommends .
In my opinion, the Attribute based approach is great if you're just starting out learning EF Core implementing a basic Todo App and such, but it always seems to cause more harm than good the minute you start working on real world complex projects.
In the API Template Pack we generally steer you down and make it easier for you to implement the Fluent API Configuration approach. In fact one of the additional approaches to Data Entity Validation I discuss in this post is provided for you out of the box when using the API Template Pack, helping you to ensure that Data Entity Validation is at the centre of your API implementation.
What is Fluent API Configuration
If you are unfamiliar with the Fluent API configuration relating the Entity Framework Core, I recommend reading the Fluent API Configuration Documentation as it is well worth familiarising yourself with it because you'll most likely start making more use of it. For several primary reasons:
- Fluent API configuration has the highest order of precedence of execution
- It overrides conventions and annotations
- Enables configuration to be specified without modifying your entity classes
- Data annotations only provide a subset of configurations available
The reasons above alone make learning how to the use the Fluent API Configuration, as the most pragmatic choice.
In the API Template Pack, we provide additional information on how to use Entity Type Configuration and some of the enhanced features to make your life easier using it.
What is Entity Validation
A key aspect of line of business applications, is to enable data input and processing to drive business decisions and outcomes. It is vital to ensure that this data collected and stored in the correct format and the data is at the time of input correct.
In order to achieve this, there are usually different types of data validation that are preformed in the separate tiers of the applications. This is loosely assimilated as:
- Front End Validation
- Middle Tier Validation
- Back end Validation
- Data Validation
Each of these phases are vitally important and none should ever be overlooked and for the safety and robustness of your applications it should never be assumed that any validation tasks will be handled in any tier over another. It is a priority that validation be executed at all tiers regardless of whether it is perceived as a duplicated effort or not. The primary reason is that there are often subtleties within the different tiers that cannot be accounted for in others.
The primary reasons to use validation in the model are to check the correctness of any one attribute/property, any whole object or any composition of objects
Implementing Domain Driven Design
Implementing Domain-Driven Design
Implementing Domain-Driven Design will impart a treasure trove of knowledge hard won within the DDD and enterprise application architecture communities over the last couple decades.
When developing applications using the Domain Driven Design approach, it is common for validation is used to accomplish different things. It is because of this it cannot be assumed that because all attributes and properties of a domain object are valid, it does not necessarily mean that the object as a whole is valid, and it does not mean the composition of objects is correct either. This is the primary reason why it is necessary to use two or more levels of validation to address all possible issues.
It is not the aim of this post to discuss the need for validation throughout the entire stack of your application, but rather we are going to focus on how to implement validation on the classes (POCO) used to define your Data Entity objects within Entity Framework, which we will refer to as Data Entities and the corresponding process of validating them as Data Entity Validation, which is also sometimes referred to as Entity Validation, but purists of DDD often see this as a slightly different aspect too.
Validation in Microservice Architecture
When implementing API services it is highly likely that your REST API services will also be interacting with other services not only via the HTTP Rest Endpoints but by gRPC and APMQ, so data will be updated and processed during these interactions. It is therefore necessary for Data Consistency and Data Validity that validation is carried, and likely that various layers maybe bypassed, so your last port of call or opportunity for any data validation will be during the commit process of the data.
Microservice Patterns
44 reusable patterns to develop and deploy reliable production-quality microservices-based applications
How to validate EF Core Entities
Typically data validation in dotnet revolves around checking that the data in a class fits within a defined set of rules. Typical rules may include checking to see whether a give field values falls within the Maximum length parameters, or whether the data value complies to some sort of criteria or whether the property has a value or not.
Take for example the following entity.
public class Sources { public string Identifier { get; set; } public string Name { get; set; } public string Domain { get; set; } public string FeedUrl { get; set; } }
We have a number of rules that we would like to apply to this Data entity to check that it complies to a criteria. For instance we would want to apply the following rules
- All fields must have value, i.e we cannot save empty values
- The identifier, Name and Domain fields must be unique values with in the table
- The Domain and Name fields cannot have the same value
- Domain must contain an Absolute URL value
- Feedurl should only contain relative URL values
- Name should not contain any Urls
- Name, Domain and Feedurl should have a maximum length of 255 characters
- Identifier should have a maximum length of 75 characters.
It is possible to actually cater for a number of these rules by simply using the Fluent API and the Entity type configuraiton
We can create a class which implements IEntityTypeConfiguration
interface,
public class SourcesConfiguration : IEntityTypeConfiguration<Sources> { public override void Configure(EntityTypeBuilder<Sources> builder) { builder.ToTable(nameof(Sources).ToLower()); builder.Property(x => x.Identifier) .HasColumnType(ColumnTypes.Varchar) .HasMaxLength(75) .IsRequired(); builder.Property(x => x.Name) .HasColumnType(ColumnTypes.Varchar) .HasMaxLength(255) .IsRequired(); builder.Property(x => x.Domain) .HasColumnType(ColumnTypes.Varchar) .HasMaxLength(255) .IsRequired(); builder.Property(x => x.FeedUrl) .HasColumnType(ColumnTypes.Varchar) .HasMaxLength(255) .IsRequired(); builder.HasIndex(x => new { x.Name, x.Domain}) .IsUnique(); builder.HasIndex(x => x.Identifier).IsUnique(); base.Configure(builder); } }
The problem is that this doesn't cater for all the rules required and we still have a find a way to implement the additional rules. We can solve by making use of the IValidatableObject
interface that is made available to us in the System.ComponentModel.DataAnnotations
namespace.
The IValidatableObject
provides a mechanism to add validation to complex objects. Lets first see how to implement it then we'll discuss the aspects of what this interface enables us to do. We'll first ensure our entity object inherits and implements the interface.
You'll notice that the interface requires you to implement a Validate method.
public class Sources : IValidatableObject { public string Identifier { get; set; } public string Name { get; set; } public string Domain { get; set; } public string FeedUrl { get; set; } public string Protocol { get; set; } public virtual ICollection<Posts> Posts { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { throw new NotImplementedException(); } }
There are three objects and properties we need to be familiar with.
- ValidationResult which typically consists of ErrorMessage and MemberNames variables. The error message and the property name will be bound automatically while the validation process, enabling you to provide list of ValidationResult into TryValidateObject method, and it internally adds the result.
- ValidationContext which is the context of validation process. It provides IServiceProvider for you to perform DI ( Dependency Injection )
- Validator which is a static helper class, which provides the
TryValidateObject
method.
Lets see an example of how you would use these methods and properties to implement a validation on your entity
public class Sources : IValidatableObject { public string Identifier { get; set; } public string Name { get; set; } public string Domain { get; set; } public string FeedUrl { get; set; } public string Protocol { get; set; } public virtual ICollection<Posts> Posts { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { if (Name == Domain) { yield return new ValidationResult("The name of the source cannot be the same as the domain name"); } if (Domain == FeedUrl) { yield return new ValidationResult($"{nameof(Domain)} and {nameof(FeedUrl)} cannot be equal to each other"); } if (!Regex.Match(Domain, RegularExpressions.DomainName, RegexOptions.IgnoreCase).Success) { yield return new ValidationResult($"{nameof(Domain)} is not valid"); } if (!Regex.Match(FeedUrl, RegularExpressions.RelativeUrlPath, RegexOptions.IgnoreCase).Success) { yield return new ValidationResult($"{nameof(FeedUrl)} is required to be relative path"); } } }
We can now write a unit test to test these assumptions, I will not write exhaustive tests at this stage but just enough to give you a flavour of the types of tests we're catering for.
public class SourcesTests { [Theory] [ClassData(typeof(SourcesTestData))] public void ShouldHaveValidationErrors(Sources source, int expectedResultCount, string reason) { var results = new List<ValidationResult>(); var context = new ValidationContext(source, null, null); Validator.TryValidateObject(source, context, results, true).ShouldBeFalse(); results.Count.ShouldBe(expectedResultCount, reason); } } public class SourcesTestData : IEnumerable<object[]> { public IEnumerator<object[]> GetEnumerator() { yield return new object[] { Builder<Sources>.CreateNew() .With(x => x.Domain = "test.com") .With(x => x.Name = "test.com") .Build(), 2, "Domain and Name have the same values and Domain is not an absolute URL" }; yield return new object[] { Builder<Sources>.CreateNew() .With(x => x.Domain = "test.com") .With(x => x.Name = "test") .Build(), 1, "Domain is not an absolute URL" }; yield return new object[] { Builder<Sources>.CreateNew() .With(x => x.Domain = "https://test.com") .With(x => x.Name = "test") .With(x => x.FeedUrl = "https://test.com") .Build(), 3, "Domain is the same as FeedUrl" }; yield return new object[] { Builder<Sources>.CreateNew() .With(x => x.Domain = "https://test.com") .With(x => x.Name = "test") .With(x => x.FeedUrl = "https://test1.com") .Build(), 2, "FeedUrl should be relative" }; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }
In order for our Database Context to use this validation we just need to override our SaveChanges
and SaveChangesAsync
methods with the following code
public override int SaveChanges() { ChangeTracker.Entries() .Where(e => e.State is EntityState.Added or EntityState.Modified) .Select(e => e.Entity) .ToList() .ForEach(entity => { var validationContext = new ValidationContext(entity); Validator.ValidateObject( entity, validationContext, validateAllProperties: true); }); return base.SaveChanges(); } public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) { ChangeTracker.Entries() .Where(e => e.State is EntityState.Added or EntityState.Modified) .Select(e => e.Entity) .ToList() .ForEach(entity => { var validationContext = new ValidationContext(entity); Validator.ValidateObject( entity, validationContext, validateAllProperties: true); }); return base.SaveChangesAsync(cancellationToken); }
Conclusion
Entity Validation is an important yet often overlooked in many EF Core based projects. I have experienced countless occasions where developers have merely only implemented the most rudimentary validation making used of the Attributes and then often failing to close the circle.
Hopefully with this post I have provided further insights and tips on how to easily achieve this in your projects.
- What is this Directory.Packages.props file all about? - January 25, 2024
- How to add Tailwind CSS to Blazor website - November 20, 2023
- How to deploy a Blazor site to Netlify - November 17, 2023