Originally published @ hashnode.
Heredia, Costa Rica, 2022-11-05
Ok, so this is something people that know me have been asking me for a long time, so I'll give you one of the best pieces of advise (and tutorial) you'll ever get for backend layered programming, modesty aside.
NOTE: This is really a small part of my larger SOLID-based layered architecture, which I haven't really published anywhere. If any part of this is unclear to you, feel free to post a question.
Introduction
First thing's first: This is for backend projects that have been programmed (or that will be programmed) in a layered fashion, which is, in this author's opinion, the only way to go. Properly structured code goes a long way.
Furthermore, I'll explain this using C#.
So during the remainder of this article, think about a layered backend project that has at least the repository layer and the service layer. If you are thinking ASP.Net, then also think about the presentation layer being the ASP.Net controllers. I am thinking ASP.Net, so this is what you'll see here.
Usual Business
By far, the most common way to program a RESTful backend layered service is to build one repository and one service for each of the individual entities defined in the database, more or less. Maybe you don't have a 1:1 relation between entities and database tables, but usually this is the case.
It is also the case, that in a typical medium-sized project, the number of entities may exceed 100. So just thinking about it you maybe rolling your eyes, thinking "here we go again, let the copy/paste party begin!" Model Features is an idea of mine that will greatly reduce your manufacturing of repository and service classes. It will even help you with validator and other types of classes too, up to some extent.
Definitions
Before proceeding any further, a couple of terms need definition.
- Model: It is individual data grouped together logically. Models are usually represented by POCO classes.
- Entity: The term is taken from Entity Relationship diagrams, which are diagrams used to describe a relational database. An entity is any model that can be uniquely identified by a primary or unique key. That's the only difference: Entity = Model with PK.
Just to make sure you get this, models could be the result of aggregating DB data (that does have PK's). Aggregation is a calculation, and is not really saved anywhere, so aggregated data is usually modeled using a model, not an entity. Such data lacks a primary key.
Model Features
Think about your data in terms of resemblance. What's the first thing that pops into your head? Tip: I already implanted it in your brain, Inception style. That's right! Most of your data can be identified by a primary key.
So, having a primary key is a feature common to many of your data definitions, and stuff like this is what I call a model feature.
A model feature is any data trait that can be leveraged on its own. That simple.
The IEntity Model Feature
So the most obvious and probably most helpful of model features is going to be called IEntity
. Why? Because we define model features as interfaces.
The IEntity model feature states that any model that implements it has a primary key, and said primary key is accessible through the
Id
property. This effectively makes the model forever an entity.
Here it is:
public interface IEntity<TKey>
where TKey : IComparable<TKey>, IEquatable<TKey>
{
TKey Id { get; set; }
}
I know, it looks scary. It is necessary, though, because in my experience, in large projects you could end up paying a super high price in database storage if you just decide all PK's will be 8 bytes in size.
But tell you what: If you are willing to pay the prize and can make all primary keys of type bigint
or whatever 8-byte data type your database supports, go for it! In that case, IEntity
can be greatly simplified to this:
public interface IEntity
{
long Id { get; set; }
}
Much better, I know.
The world, however, doesn't work as simply as that, so I'll pretend I don't know about this simplified version of IEntity
and will continue explaining based on the first, generic version.
The TKey Type Parameter
Basically we are saying: Entities will have an Id
property that is the entity's primary key, but the key's data type is varied.
The type's restrictions in the where
clause are there to make sure we can actually have the ability to sort lists based on primary key (IComparable<TKey>
) and to find particular keys in collections of entities (IEquatable<TKey>
). This could be the case when you cache relatively small quantities of reference data (countries, regions, languages, etc.) in order to minimize database usage.
NOTE: In reality I create a base
Entity<TKey>
base class that implementsIComparable<Entity<TKey>>
,IEquatable<Entity<TKey>>
andIEquatable<TKey>
, but I will purposely leave it out of this already lengthy article.
IEntity Examples
If we had a table named Countries
, and knowing there are less than 256 countries (but we are close), we could define the table's primary key as tinyint
or whatever type it is in your database that uses just one byte. In reality, I'd say we're too close to the limit, so personally I would use a 2-byte data type. In C#, that would be:
public class Country : IEntity<short>
{
public short Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
}
As a second example, let's model the table Users
.
public class User : IEntity<int>
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string GivenName { get; set; }
public string SurName { get; set; }
public string Name { get; set; }
}
Does this make sense? Are you following? I hope you are. If not, feel free to drop a comment.
Ok, let's see how my Inception skills are working on you. After examining the above models, can you tell me about a model feature that can be extracted?
The INamed Model Feature
Congratulations for all of you that inferred this model feature from the above IEntity
examples. That's right: Both models boast the Name
property, and it would be nice to have it as a model feature because it is very common for user interfaces to provide users the ability to search for things by name. We haven't seen how model features help produce code yet, but trust me on this one: This is a super helpful model feature.
The INamed model feature states that any model that implements it has a (human-readable) name, and said name is accessible through the
Name
property of typestring
.
Here it is:
public interface INamed
{
string Name { get; set; }
}
INamed Examples
Let's update the examples from IEntity
:
For Country
:
public class Country : IEntity<short>, INamed
{
public short Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
}
For User
:
public class User : IEntity<int>, INamed
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string GivenName { get; set; }
public string SurName { get; set; }
public string Name { get; set; }
}
Ok, I'll stop here with the model features. I hope that, with these 2 model feature examples, you have successfully grasped the concept and that you will be able to successfully define model features on your own.
I could go on with more model features if demand is high in another article. For example, the IAlternateKey
model feature that actually allows for UPSERT operations, or the IActivatable
model feature that allows for soft-deletion. Anyway, there's really no limits to the number of model features one could create. Enough of this, let's see how model features help write code.
Writing Base Code Based On Model Features
We went through the trouble of defining model features not because we had nothing better to do, but because we have a higher purpose: To more easily manufacture repositories and services. Never lose track of your objective.
RESTful Core Functions
In a RESTful service, we usually provide the following core features per resource (entity):
- Get the entire collection of a resource.
- Get an individual resource by primary key.
- Add an individual resource.
- Update an individual resource.
- Delete an individual resource.
Depending on the case, more or less features are implemented or allowed.
The Repository Base Class
Just by using the IEntity
model feature, we can implement a base repository class that implements 3 of the core features right away, in a complete fashion: Get all, get single and delete.
NOTE: The code is actually incomplete because I purposedly left away the selection of an ORM. Depending on your ORM selection, your base class would need services and other data injected through its constructor, depending on your selection.
public abstract class EntityRepository<TEntity, TKey>
where TEntity : IEntity<TKey>
where TKey : IComparable<TKey>, IEquatable<TKey>
{
public Task<IEnumerable<TEntity>> GetAllAsync()
{
// Use Dapper or EF or your favorite ORM to get all.
}
public Task<TEntity> GetAsync(TKey id)
{
// Use your ORM to get by ID. You can access the Id property on variables of type TEntity.
// I'll write LINQ to demonstrate, assuming EF.
return _dbSet.Where(e => e.Id.Equals(id)).SingleOrDefaultAsync();
}
public async Task<TEntity> DeleteAsync(TKey id)
{
// Use your ORM to delete. Personally, I always GET before deleting, and I return
// the last known version of the entity being deleted.
TEntity toBeDeleted = await GetAsync(id);
// Now delete using your ORM.
}
}
Now, using this base, you can actually create repositories without incurring in a copy/paste, DRY-violating party.
public class CountryRepository : EntityRepository<Country, short>
{ }
public class UserRepository : EntityRepository<User, int>
{ }
Those repositories are fully functional on the 3 core REST functions aforementioned. Have I caught your attention already? Once an ORM is selected, you can actually write code in the base class to complete the other 2 REST functions. At least I can attest to this if the ORM is my beloved Dapper, or the annoying Entity Framework (why it annoys me).
The Service Base Class
Ok, there's a trick I haven't mentioned so far that is needed to create the service base class. Basically we need a data type that we can use to define the respository we need. You could think Generics plus a where
clause that ensures it is a derived type of the repository base class. This is not good because there will be repositories out there that might not have inherited from that class.
So the trick is to actually prepare a base repository interface, and have the base repository implement it.
public interface IEntityRepository<TEntity, TKey>
where TEntity : IEntity<TKey>
where TKey : IComparable<TKey>, IEquatable<TKey>
{
Task<IEnumerable<TEntity>> GetAllAsync();
Task<TEntity> GetAsync(TKey id);
Task<TEntity> DeleteAsync(TKey id);
}
Now update the base repository class:
public abstract class EntityRepository<TEntity, TKey> : IEntityRepository<TEntity, TKey>
// Etc. The rest is the same.
Now we have a suitable type for our repository object in the service base class.
public abstract class EntityService<TEntity, TKey, TRepository>
where TEntity : IEntity<TKey>
where TKey : IComparable<TKey>, IEquatable<TKey>
where TRepository : IEntityRepository<TEntity, TKey>
{
// Our repository object, usually passed via the constructor using dependency injection.
private readonly TRepository _repository;
public virtual Task<IEnumerable> GetAllAsync()
=> _repository.GetAllAsync();
public virtual Task<TEntity> GetAsync(TKey id)
=> _repository.GetAsync(id);
public virtual Task<TEntity> DeleteAsync(TKey id)
=> _repository.DeleteAsync(id);
}
Finally, just like we did with repositories, we declare concrete classes per entity:
public class CountryService : EntityService<Country, short, ???>
{ }
public class UserService : EntityService<User, int, ???>
{ }
Oh oh!! What happend? We are missing the repository data types. Yes, we could have used CountryRepository
and UserRepository
and it would have worked, but it is dependency injection's best practice (for reasons not discussed here) to inject interfaces, not concrete classes.
To actually finish this properly, we must define interfaces for each repository:
public interface ICountryRepository : IEntityRepository<Country, short>
{ }
public interface IUserRepository : IEntityRepository<User, int>
{ }
Let's update the repository classes:
public class CountryRepository : EntityRepository<Country, short>, ICountryRepository
{ }
public class UserRepository : EntityRepository<User, int>, IUserRepository
{ }
That wasn't too bad. Now, let's finish:
public class CountryService : EntityService<Country, short, ICountryRepository>
{ }
public class UserService : EntityService<User, int, IUserRepository>
{ }
Just as before when we did the concrete repository classes, these are fully functional service classes.
About the Oversimplificaitons in the Service Base Class
As I have already mentioned, this is just a small part of my considerably larger SOLID-based layered architecture. Without explaining other parts of my architecture, I cannot possibly show a more elaborate service base class, for example, including the execution of data validators, or how to standardize the service methods' return types (because just returning the entity doesn't cut it in realistic projects).
I had at least made the methods virtual
so inheritors can add some business logic to them by overriding.
Conclusion
I know some of you will feel that we did not cover everything. We did not. You are correct. I could write an entire book about this and other best practices while I explain my architecture. If you enjoyed the content, let me know by commenting.
I will try to anticipate the most popular question: Why a base class? What is all that about composition over inheritance, and how do we create base classes or base code for other model features? The short answer is: Depends on your programming language. C# is feature-rich, and I usually go for a base class for the most common model features in the project. On the other hand, C# disallows multiple base class inheritance, so it is perfectly possible that you'll have to make some composition for certain model feature combinations. In the end, what's important is to achieve performant, DRY-compliant source code.
I hope you have enjoyed this reading. This is not something you'll find anywhere else. This is from me to you, hopefully encouraging you to learn the abilities your programming language of choice provides and hopefully encouraging your creativity. But above all that, demonstrating that DRY is quite possible in many, many scenarios, no matter how complex they seem to be.
Happy coding.