This is my first CRUD app, and I am trying to determine the best way to implement a clean architecture to access the more than 20 tables I currently have in my database. I am using Dapper as I want to develop my SQL skills and I have run into the problem of how to structure my Service & Repository Layers. I know I could just write the methods for Inserts, Deletes, etc inside their respective interfaces and implementations and be done with it, but I want to write this app using best practices and have therefore attempted to go deeper into using Interfaces. The problem I have is that while defining an IBaseRepository interface that contains methods for Insert, Get, GetAll, etc, I ran into the problem that some of my repositories shouldn’t have a "Delete" or "Update" method as there should be no reason my case to edit or delete any rows from an Audit or Log table.
Before attempting to make any changes this was the general outline of a basic Repository.
public interface ICompanyRepository
{
Task<ICollection<CompanyModel>> GetCompanyModelsAsync();
Task<OperationResult> DeleteCompanyAsync(int companyId);
Task<CompanyModel> GetCompanyModelAsync(int companyId);
Task<OperationResult> UpdateCompanyAsync(CompanyModel companyModel);
}
public class CompanyRepository : ICompanyRepository
{
private readonly IConfiguration _config;
public CompanyRepository(IConfiguration config)
{
_config = config;
}
public async Task<OperationResult> DeleteCompanyAsync(int companyId)
{
using IDbConnection connection = new SqlConnection(_config.GetConnectionString("Default"));
string query = "DELETE FROM Company WHERE Id = @CompanyId";
var parameters = new { CompanyId = companyId };
try
{
int rows = await connection.ExecuteAsync(query, parameters);
if (rows > 0)
{
return OperationResult.Success();
} else
{
return OperationResult.DeleteWithNoRowsEffected();
}
} catch (Exception ex)
{
Log.Error(ex, "Error deleting company {companyId}.", companyId);
return OperationResult.Failure($"An error has occured: {ex.Message}");
}
}
...
To solve my problem, I decided to make various sub-interfaces that would be implemented by the IRepository interfaces. Here are the sub-interfaces:
public interface ICreatableTableRepository<T>
{
Task<OperationResult> CreateModelAsync(T model);
}
public interface IDeletableTableRepository<T>
{
Task<OperationResult> DeleteModelAsync(int id);
}
public interface IFetchableTableRepository<T>
{
Task<ICollection<T>> GetModelsAsync();
Task<T> GetModelAsync(int id);
}
public interface IMutableTableRepository<T>
{
Task<OperationResult> UpdateModelAsync(T model);
}
And here is an example of how I would use them in a repository which shouldn’t ever be deleting or changing rows.
public interface ILocationUploadRepository : IFetchableTableRepository<LocationUploadModel>, ICreatableTableRepository<LocationUploadModel>
{
}
A scenario that outlines my problem well and which has likely been encountered many times already is performing CRUD operations against static tables that just emulate Enums. In this case, defining a delete method that is certain to produce a Foreign Key Constraint exception for likely every row seems nonsensical. But, defining a create method is very reasonable, and therefore doesn’t fit into a fetchable only or writable only class, but rather a mixture.
Am I making unnecessary complications in my code base, or is this useful? Again this is my first app so I have little to no experience developing full projects and am looking for feedback.
2
Answers
Not useful. It’s not a repository concern that some entities are normally not created or deleted. If an entity is not to be deleted, then no service type should have a method that performs the delete. To follow your example of an Audit entity, the Logging service would have a LogEvent() method, but no method that would delete rows.
And there may always be unusual operations that call for the delete, like a process to upgrade your application that performs maintenance on these enums, or trims the older Audit event entries.
Don’t get hung up on best practice’s, work on starting and more importantly get everything working, then look at optimizing.
There is 1000 was to do something, worrying about getting it right first time will make you go round and round in circles.
The next time you do it, think is the last way i did it working is not use something else.