Feature Proposal : interceptors
Motivations to Implement this feature.
OrmLite Insert, Update Filters are well, however they can lead into few limitations.
- Large switch statements to check if the entity is the desired to process.
- Put all the logic in a single place, could lead in a very large and unreadable code.
- There is no flexibility to add behaviour as a new functionality need to merged with the current delegated or it will be erased.
Proposal
Interceptors.
They are basically classes that implement C# IEntityInterceptor interface.
/// <summary>
/// Represent a plugin to perform operations for a given entity.
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IEntityInterceptor<in T> : IEntityInterceptor
{
/// <summary>
/// Execute code before create a new entity.
/// </summary>
/// <param name="dbCmd"></param>
/// <param name="entity"></param>
/// <returns></returns>
Task OnInsertAsync(IDbCommand dbCmd, T entity);
/// <summary>
/// Execute code before update an entity.
/// </summary>
/// <param name="dbCmd"></param>
/// <param name="entity"></param>
/// <returns></returns>
Task OnUpdateAsync(IDbCommand dbCmd, T entity);
}
This will be executed for each Insert, Update call for an entity. Benefits using this approach.
- Fully Async Code for Interception
- 100% Compatible with Async, 0 breaking change.
- Multiple classes with different responsibilities (+ Readability +Separation of Concerns)
- Flexibility to assign more behaviour without break any existing code.
- More testable code that change entities through interception.
At code level
- Filters more controlled in a single place if in the future more operations needs to be performed.
Code implemented and unit tests added.
Usage:
[Alias("client")]
public class Client
{
public string Email { get; set; }
public string Domain { get; set; }
}
public class : IEntityInterceptor<EmailAddressesPoco>
{
public string Name => "InterceptorName";
public Task OnInsertAsync(IDbCommand dbCmd, Client entity)
{
return Task.Completed.
}
public Task OnUpdateAsync(IDbCommand dbCmd, Client entity)
{
entity.Domain = entity.Email.Split('@')[1];
return Task.Completed;
}
}
Registration is straight forward
OrmLiteConfig.AddInterceptor(new NewInterceptor());
Feature proposal:
- Support Interceptors for base/abstract classes for common usage cases like Auditable classes.
- Allow run interceptions in parallel with a configuration.
- New overload with the connection in Interceptors, that way the user can perform operations from an Interceptor for related entities or recursive loops.
Things I don't like about this impl:
- It forces all "interceptors" to have a
Nameand implement bothOnInsertAsyncandOnUpdateAsyncmethods - It runs Async over Sync which is never a good idea and why OrmLite has separate code paths for async
- Its more verbose than what exists already
OrmLite already has OrmLiteConfig.InsertFilter and OrmLiteConfig.UpdateFilter I'm happy to add equivalent OrmLiteConfig.InsertFilterAsync and OrmLiteConfig.UpdateFilterAsync that gets called in Async APIs (with default impl that falls back to executing sync filters) that way you can override them to use your Interceptor pattern outside of OrmLite.
I could even add a list of typed filter callbacks similar to Typed Request Filters but don't want to bake this opinionated impl in OrmLite, but you should be able to implement it with the filters OrmLite makes available.
@mythz hey Demis, great thanks for you feedback.
- Name could be easily removed,
- I did with an interface to avoid coupling however we could add a Base Interceptor with no behaviour to only implement those methods you need.
About Async over Sync you're right I was thinking a lot about it, the problem is that if you have the Async and Sync methods is weird that if your interceptor is Async and your are performing a Sync Insert they are not being called. One think I'm thinking now is have IAsyncInterceptor and IInterceptor and Async interceptor could be called only async operations.
Let me figure it out a little bit more this.
Thanks again for you feedback :) I was facing a few scenarios where I needed something like this and thought it could be a good idea to bring to the SDK.
Cleaning up old PRs