For a toy project I had to write a very small GRPC service to store usernames and passwords in a database. I’ve decided to use .netcore with EntityFramework core and a SqLite db. I wanted to check the supplied password to be present, hash it and convert it to a base64 encoded string to save to the DB.
So I’ve created a PasswordAttribute inheriting from ValidationAttribute
internal class PasswordAttribute : ValidationAttribute
{
public PasswordAttribute()
: base("Error.EmptyPassword")
{
}
public override bool IsValid(object value)
{
if (value is string pwd)
return pwd.Length > 0;
return false;
}
}
and a PasswordConverter inheriting from ValueConverter
internal sealed class PasswordConverter : ValueConverter<string, string>
{
private static readonly Expression<Func<string, string>> Encode =
x => new string(CreatePasswordHash(x));
private static readonly Expression<Func<string, string>> GetHash = x => x;
public PasswordConverter(
ConverterMappingHints mappingHints = null)
: base(Encode, GetHash, mappingHints)
{
}
private static string CreatePasswordHash(string x)
{
return new HashPassword().CreatePasswordHash(x);
}
}
The HashPassword creates a salted password hash with a slow algorithm. Of course, it could also be supplied by DI.
My model looked like
public class User
{
[Key] public string EMailAddress { get; set; }
public bool IsDeleted { get; set; }
[Password] public string Password { get; set; }
}
At that moment I was aware I had to register the converter in the OnModelCreation method.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
var entityProperties = GetAllEntityProperties(modelBuilder);
RegisterConverters(entityProperties);
}
private static IEnumerable<IMutableProperty> GetAllEntityProperties(ModelBuilder modelBuilder)
{
return modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetProperties());
}
private static void RegisterConverters(IEnumerable<IMutableProperty> propertyType)
{
foreach (var property in propertyType)
RegisterPasswordConverter(property);
}
private static void RegisterPasswordConverter(IMutableProperty property)
{
if (PropertyHasPasswordAttribute(property))
property.SetValueConverter(new PasswordConverter());
}
Interestingly the test for validation and conversion failed. At first I thought this was caused by the in-memory implementation of the SqLite DB I was using in the tests. However, running the same tests with a normal SqLite DB implementation did not succeed as well. But at least I could check the table. There the password was saved hashed. Retrieving it in code gave me the plain text value. It turned out I had forgotten to detach the entity to make sure it was always loaded from the DB and not from the cache. This meant to override the SaveChanges
and SaveChangesAsync
methods to detach an entity if a property has a PasswordAttribute
set.
So far so good, but this did not implement the validation. Why was the entity not validated on saving? Searching the internet I always just found tutorials on how to use validation in MVC/Razor context but not on GRPC services. Good for me Brice Lambson wrote an article on exact that problem. Seems EF Core does no validation as it assumes the validation was already performed by MVC. I just had to hack it back into the SaveChanges
and SaveChangesAsync
.
In the end my UserContext looked like this:
public class UserContext : DbContext
{
public UserContext(DbContextOptions<UserContext> options)
: base(options)
{
}
public DbSet<User> Users { get; set; }
public int SaveChanges(bool validateOnSave = true)
{
Validate(validateOnSave);
var result = base.SaveChanges();
PostProcessChangedEntities();
return result;
}
public async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess = true,
bool validateOnSave = true,
CancellationToken cancellationToken = new CancellationToken())
{
Validate(validateOnSave);
var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
PostProcessChangedEntities();
return result;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
var entityProperties = GetAllEntityProperties(modelBuilder);
RegisterConverters(entityProperties);
}
private static void DetachOnPasswordAttributePresent(EntityEntry entityEntry)
{
if (entityEntry.Properties.Any(p => PropertyHasPasswordAttribute(p.Metadata)))
entityEntry.State = EntityState.Detached;
}
private static IEnumerable<IMutableProperty> GetAllEntityProperties(
ModelBuilder modelBuilder)
{
return modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetProperties());
}
private IEnumerable<object> GetModifiedEntities()
{
return from e in ChangeTracker.Entries()
where e.State == EntityState.Added
|| e.State == EntityState.Modified
select e.Entity;
}
private void PostProcessChangedEntities()
{
var changedEntries = ChangeTracker.Entries();
foreach (var entityEntry in changedEntries)
DetachOnPasswordAttributePresent(entityEntry);
}
private static bool PropertyHasPasswordAttribute(IProperty property)
{
return property
.PropertyInfo
.GetCustomAttributes(typeof(PasswordAttribute), false)
.Any();
}
private static void RegisterConverters(IEnumerable<IMutableProperty> propertyType)
{
foreach (var property in propertyType)
RegisterPasswordConverter(property);
}
private static void RegisterPasswordConverter(IMutableProperty property)
{
if (PropertyHasPasswordAttribute(property))
property.SetValueConverter(new PasswordConverter());
}
private void Validate(bool validateOnSave)
{
if (validateOnSave)
{
var entities = GetModifiedEntities();
ValidateEntites(entities);
}
}
private static void ValidateEntites(IEnumerable<object> entities)
{
foreach (var entity in entities)
{
var validationContext = new ValidationContext(entity);
Validator.ValidateObject(
entity,
validationContext,
true);
}
}
}