I'm going to present a unique solution to this problem.
In my use-case I wanted greater control over which fields are auto-truncated.
So, I created an attribute to annotate the entity properties I want truncated:
/// <summary>
/// Indicates that the assigned string should be automatically truncated to the
/// max length of the database column as specified by
/// <see cref="System.ComponentModel.DataAnnotations.MaxLengthAttribute"/>
/// </summary>
public class AutoTruncateAttribute : Attribute { }
public class TestEntity
{
[MaxLength(10), AutoTruncate]
public string SomeProperty { get; set; }
}
Next, I needed a way to actually truncate the entity. I want this to happen automatically as a pre-process step when the entity is saved.
So, I override the DbContext.SaveAs()
methods and added a stub method for my truncation functionality:
public class DbContext
{
public DbContext() : base("name=DbContext") { }
public DbSet<TestEntity> TestEntities { get; set; }
public override int SaveChanges()
{
PreProcessEntities();
return base.SaveChanges();
}
public override Task<int> SaveChangesAsync()
{
PreProcessEntities();
return base.SaveChangesAsync();
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken)
{
PreProcessEntities();
return base.SaveChangesAsync(cancellationToken);
}
/// <summary>Process entities before save</summary>
public void PreProcessEntities()
{
// my pre-processing functionality will go here...
}
}
I could stop at this point and just copy the code from the other answers and paste it into the PreProcessEntities()
method. Eg I could loop through the added and modified entities, get the entity type, find properties with the AutoTruncate
, and MaxLength
attributes, then truncate the property value if needed.
BUT! There are a couple of problems with this approach.
- Lack of flexibility to add additional pre-processing.
Eg In my case, I also want some string properties containing numerical values to be to be formatted in different ways.
- Looping through all the entities and properties and finding these attributes every time I run
SaveAs()
must give a noticeable performance hit.
So I wondered if it possible to cache the actions to be performed for each entity type. Turns out... there is :). It takes a bit of code but is easy enough to follow and implement.
Firstly, I need a thread-safe singleton to cache the actions for each entity. Then I want a class or method that can analyse an entity class and its attributes. The result of the analysis will be a list of actions to be applied to instances of that type before being saved to the database.
I created three classes/interfaces:
IEntityProcessor
is an interface that accepts an entity and performs some action on it
AutoTruncateProcessor
implements IEntityProcessor
and truncates the entity's properties which have been marked with AutoTruncate
and MaxLength
EntityProcessorChecker
analyses an entity for certain attributes such as AutoTruncate
and produces a list of IEntityProcessor
instances.
EntityProcessorCache
maintains a thread-safe cache of IEntityProcessor
for each entity.
Naming isn't my strong suit, hopefully you get the idea though.
First, I'll show you the implementation of `DbContext.PreProcessEntities().
This can be implemented either using generics or not.
The first approach is without generics. The drawback is that we are accessing the cache for every entity being added or modified. This which may increase the hit to performance.
// without generics
public void PreProcessEntities()
{
var entities = ChangeTracker.Entries<T>()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)
.ToList();
foreach (var entity in entities)
{
// drawback: access cache for every entity
var processors = EntityProcessorCache.GetProcessors(entity);
foreach (var processor in processors)
{
processor.Process(entity.Entity);
}
}
}
The second approach uses generics. The drawback is we need to explicitly call our generic method with explicit types. Maybe there's a way around this using reflection. The benefit is the cache is accessed only once per type, and an early exit if there are no actions.
// using generics
public void PreProcessEntities()
{
// drawback: we need to remember to explicitly add new entity types here,
// or find a dynamic solution using reflection
PreProcessEntities<TestEntity>();
}
public void PreProcessEntities<T>() where T : class
{
// benefit: access cache at the start
var processors = EntityProcessorCache.GetProcessors<T>();
if (!processors.Any()) return; // benefit: early exit
// benefit: only processing entities of the given type
var entities = ChangeTracker.Entries<T>()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)
.ToList();
foreach (var entity in entities)
{
foreach (var processor in processors)
{
processor.Process(entity.Entity);
}
}
}
Finally, here is the implementation for IEntityProcessor
, AutoTruncateProcessor
, EntityProcessorChecker
, and EntityProcessorCache
.
public interface IEntityProcessor
{
void Process(object entity);
}
public class AutoTruncateProcessor : IEntityProcessor
{
IDictionary<System.Reflection.PropertyInfo, int> PropertyMaxLengths { get; }
public AutoTruncateProcessor(List<System.Reflection.PropertyInfo> autoTruncateProperties)
{
PropertyMaxLengths = new Dictionary<System.Reflection.PropertyInfo, int>();
// pre-compute values for the properties that should be truncated
foreach (var property in autoTruncateProperties)
{
var customAttributes = property.GetCustomAttributes(true);
var maxLengthAttribute = customAttributes.FirstOrDefault(a => a is MaxLengthAttribute) as MaxLengthAttribute;
var maxLength = maxLengthAttribute?.Length;
if (maxLength.HasValue) PropertyMaxLengths.Add(property, maxLength.Value);
}
}
public void Process(object entity)
{
// use the pre-compute values to process entity
foreach (var kv in PropertyMaxLengths)
{
var property = kv.Key;
var maxLength = kv.Value;
var currentValue = property.GetValue(entity) as string;
// exit early
if (string.IsNullOrEmpty(currentValue )) return;
if (currentValue .Length < maxLength) return;
var newValue = str.Substring(0, maxLength);
property.SetValue(entity, newValue);
}
}
}
The EntityProcessorChecker
looks through the properties of the given type and instantiates subclasses of IEntityProcessor
when it finds matching attributes. This is where you customise the functionality based on your own applications needs. In my implementation I want to look for AutoTruncate
and NumberFormat
attributes.
public class EntityProcessorChecker
{
IList<IEntityProcessor> Processors { get; }
public EntityProcessorChecker(Type type)
{
Processors = new List<IEntityProcessor>();
var properties = type.GetProperties();
// get properties where there is an AutoTruncateAttribute
var autoTruncateProperties = properties.Where(p => p.GetCustomAttributes(true).Any(a => a is AutoTruncateAttribute)).ToList();
if (autoTruncateProperties.Any()) Processors.Add(new AutoTruncateProcessor(autoTruncateProperties));
// get properties where there is a number formatter
var formatterProperties = properties.Where(p => p.GetCustomAttributes(true).Any(a => a is NumberFormatAttribute)).ToList();
// TODO: add this processor
}
public IList<IEntityProcessor> GetProcessors() => Processors;
}
// Some people may want a generic version of this class
public class EntityProcessorChecker<T> : EntityProcessorChecker
{
public EntityProcessorChecker() : base(typeof(T)) { }
}
The EntityProcessorCache
means we don't need to continually analyse entity types for the actions we need to perform on their instances. It's an optimisation step. The singleton design was inspired by this article.
// sealed so that we can ensure it remains a singleton
public sealed class EntityProcessorCache
{
// private concurrent dictionary for thread safety
private static readonly ConcurrentDictionary<Type, IList<IEntityProcessor>> Cache = new ConcurrentDictionary<Type, IList<IEntityProcessor>>();
// a lock for when we intend to write to the cache
private static readonly object CacheWriteLock = new object();
// Explicit static constructor to tell C# compiler
// not to mark type as `beforefieldinit`
static EntityProcessorCache() { }
// the only way to access the cache
public static IList<IEntityProcessor> GetProcessors(Type type)
{
// return early if cache is populated
if (Cache.ContainsKey(type)) return Cache[type];
// lock cache writing writing
lock (CacheWriteLock)
{
// another caller may have locked before this call
// return early if cache is now populated
if (Cache.ContainsKey(type)) return Cache[type];
// analyse the type and cache the list of `IEntityProcessor`
var checker = new EntityProcessorChecker(type);
var processors = checker.GetProcessors();
Cache[type] = processors;
return processors;
}
}
// alternatively, pass in a typed instance
public static IList<IEntityProcessor> GetProcessors(object obj) => GetProcessors(obj.GetType());
// alternatively, use generics
public static IList<IEntityProcessor> GetProcessors<T>() => GetProcessors(typeof(T));
}
Wow that was a lot of code and explanation!
Hopefully you can see some benefits in this approach.
I like that I can quickly and easily create new attributes to mark up my entity properties and processers to modify my entities before save.
There is of course plenty of room for improvements:
EntityProcessorChecker
should probably be refactored to make the analysis process more extensible and explicitly obvious when looking at an entity. It should be easy to define and check for new attributes and create corresponding processors.
EntityProcessorCache
was designed the way it was because of how the DbContext
is built in .Net Framework. In .Net Core, I presume we could use the built-in DI system to create and manage the singleton instance instead. As long as the DbContext
is able to access it, I assume the cache would be passed in via the constructor.