Interfaces for Data
We are all used to creating interfaces for our Services. In many cases we go crazy creating too many services for things that could just be static functions. But that's a topic to discuss another time. Rather today let's talk about interfaces for our data. By creating interfaces for our data, we can create shared contracts that can be used to make our methods/functions more reusable.
For example, it is not uncommon to have some version of these 4 properties on several models:
- CreatedDate
- CreatedBy
- LastUpdatedDate
- LastUpdatedBy
If we specify these properties in an Interface, we can create methods that act on any class that inherits from that interface.
interface IAuditable { public DateTime CreatedDate { get; set; } public string CreatedBy { get; set; } public DateTime LastUpdatedDate { get; set; } public string LastUpdatedBy { get; set; } } class MyModel : IAuditable { public DateTime CreatedDate { get; set; } ... }
This allows for better reusability of our methods. Instead of creating methods that only act on MyModel
we can create generic methods that work on any class that inherits IAuditable.
static IQuerable<IAuditable> OrderByCreatedDate(this IQueryable<IAuditable> query) { return query.OrderBy(_ => _.CreatedDate); }
The reason I say to use an interface is because a class can only implement a single base class at a time. Some people get around this by creating nested base classes, but that makes things more difficult to find and you end up traversing several nested base classes to get to what you are looking for.
In this particular example, this interface also works well to let our DbContext handle setting the values for us:
public class MyDatabaseContext : DbContext { ... public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) { this.ChangeTracker.DetectChanges(); var added = this.ChangeTracker.Entries() .Where(_ => _.State == EntityState.Added) .Select(_ => _.Entity) .ToArray(); foreach (var entity in added) { if (entity is IAuditable) { var track = entity as IAuditable; track.CreatedDate = DateTime.UtcNow; track.CreatedBy = GetUserId(); } } var modified = this.ChangeTracker.Entries() .Where(_ => _.State == EntityState.Modified) .Select(_ => _.Entity) .ToArray(); foreach (var entity in modified) { if (entity is IAuditable) { var track = entity as IAuditable; track.LastUpdatedDate = DateTime.UtcNow; track.LastUpdatedBy = GetUserId(); } } return base.SaveChangesAsync(); } }
Interfaces on our data let us write methods that can be much more reusable across all data models that share similar properties. Instead of writing all of our methods against concrete data models, and creating numerous 'base' models, can we simplify and flatten our data structure by using interfaces and gain better reusability?