Ok. Today I have extended the validation part a bit. The code is about 90% the same from the post I wrote yesterday (Entity framework 4 – Part 5 – Validation using Data Annotations). But I have now added some custom data annotation attributes and made it possible for the services to add custom validation of the entities.
I will not show all the code since much of it is covered in Entity framework 4 – Part 5 – Validation using Data Annotations and as always, you can download a complete code example.
The EntityValidator
It uses the builtin Validator and ValidationResult found in System.ComponentModel.DataAnnotations. It validates sent entities by looking at your entity for validation attributes. I have now also made it possible to inject a Func, which lets you perform custom validation. What I medan by this is that you can provide a func that generates validation results that gets merged into the validation results generated by the validation attributes. The Func takes the Entity and a bool as in-params. The bool contains true if the validation attributes resulted in a valid entity. The result of the Func should be an IEnumerable, which will be merged by the validationresults generated by the validation attributes.
public class EntityValidator<T> where T : IEntity
{
public EntityValidationResult Validate(T entity, Func<T, bool, IEnumerable<ValidationResult>> customValidation = null)
{
var validationResults = new List<ValidationResult>();
var vc = new ValidationContext(entity, null, null);
var isValid = Validator.TryValidateObject(entity, vc, validationResults, true);
if (customValidation != null)
validationResults.AddRange(customValidation(entity, isValid));
return new EntityValidationResult(validationResults);
}
}
The Service
To ease things in my services I have implemented a helper method in a base-class which my services extends. This method will only invoke the customvalidation Func if the validation attributes haven’t generated an invalid entity.
public abstract class Service
{
protected IEntityStore EntityStore { get; private set; }
protected Service(IEntityStore entityStore)
{
EntityStore = entityStore;
}
protected EntityValidationResult ValidateEntity<T>(T entity, Func<T, IEnumerable<ValidationResult>> customValidation = null)
where T : IEntity
{
Func<T, bool, IEnumerable<ValidationResult>> customValidationProxy = null;
if (customValidation != null)
customValidationProxy = (e, isValid) => isValid ? customValidation(e) : null;
return new EntityValidator<T>().Validate(entity, customValidationProxy);
}
}
In my Security-service I provide some custom validation to check if the Email isn’t allready taken, by providing a customvalidation Func. I also make use of some extension methods to get access to named queries for my UserAccount entities.
public class SecurityService : Service, ISecurityService
{
public SecurityService(IEntityStore entityStore)
: base(entityStore)
{
}
public ServiceResponse<UserAccount> SetupNewUserAccount(UserAccount userAccount)
{
var validationResult = ValidateEntity<UserAccount>(userAccount, CustomValidationForSettingUpNewAccount);
if (!validationResult.HasViolations)
{
EntityStore.AddEntity(userAccount);
EntityStore.SaveChanges();
}
return new ServiceResponse<UserAccount>(userAccount, validationResult);
}
private IEnumerable<ValidationResult> CustomValidationForSettingUpNewAccount(UserAccount userAccount)
{
var violations = new List<ValidationResult>();
var emailIsTaken = EntityStore.EmailIsTakenByOther(userAccount.Username, userAccount.Email);
if (emailIsTaken)
violations.Add(new ValidationResult("Email is allready taken."));
return violations;
}
}
Named queries – Extension methods to my Entitystore
Since I don’t use a custom implementaion of a Entitystore for the application (although there is one provided in the example code), I have implemented my specific entity queries as extension methods. So if I want access to e.g. specific queries for my useraccounts, I just import the namespace where they are located (Sds.Christmas.Storage.Queries.UserAccounts).
using Sds.Christmas.Storage.Queries.UserAccounts;
public static class UserAccountQueries
{
public static bool EmailIsTakenByOther(this IEntityStore entityStore, string username, string email)
{
return
entityStore.Query<UserAccount>().Where(
u =>
u.Username.Equals(username, StringComparison.InvariantCultureIgnoreCase) &&
u.Email.Equals(email, StringComparison.InvariantCultureIgnoreCase)).Count() > 0;
}
}
Custom data annotaions
Since my UserAccount contains an Email I have created a custom Email-validationattribute and applied it to the Email-property. The errormessages are retrieved via resx-files.
[Serializable]
public class UserAccount
: Entity
{
[Required(AllowEmptyStrings = false, ErrorMessageResourceName = "UserAccountRequiredUsername", ErrorMessageResourceType = typeof(ModelValidationMessages))]
[StringRange(MinLength=5, MaxLength=20, ErrorMessageResourceName = "UserAccountInvalidLength", ErrorMessageResourceType = typeof(ModelValidationMessages))]
public virtual string Username { get; set; }
[Required(AllowEmptyStrings = false, ErrorMessageResourceName = "UserAccountRequiredPassword", ErrorMessageResourceType = typeof(ModelValidationMessages))]
public virtual string Password { get; set; }
[Required(AllowEmptyStrings = false, ErrorMessageResourceName = "UserAccountRequiredEmail", ErrorMessageResourceType = typeof(ModelValidationMessages))]
[Email(ErrorMessageResourceName = "UserAccountEmailHasInvalidFormat", ErrorMessageResourceType = typeof(ModelValidationMessages))]
public virtual string Email { get; set; }
}
[Serializable]
public class EmailAttribute : RegexAttribute
{
public EmailAttribute() : base(@"^[\w-\.]{1,}\@([\w]{1,}\.){1,}[a-z]{2,4}$", RegexOptions.IgnoreCase)
{}
}
[Serializable]
public class RegexAttribute : ValidationAttribute
{
public string Pattern { get; set; }
public RegexOptions Options { get; set; }
public RegexAttribute(string pattern, RegexOptions options = RegexOptions.None)
{
Pattern = pattern;
Options = options;
}
public override bool IsValid(object value)
{
return IsValid(value as string);
}
public bool IsValid(string value)
{
return string.IsNullOrEmpty(value) ? true : new Regex(Pattern, Options).IsMatch(value);
}
}
If you now try to add two different useraccounts with the same Email, you will not succed.
//Setup new useraccount using Service var userAccount = SetupNewUserAccount(); var userAccount2 = SetupNewUserAccount(); //=> Gives Email is allready taken error.
That’s it. Don’t forget to download and explore the code.
//Daniel
Pingback: Extend IQueryable instead of a certain dataprovider – more decoupled code « Daniel Wertheim