Introduction to interceptors: understanding the basics
In the complex world of software development, it is crucial to make code not only efficient, but also extensible and easy to maintain. Interceptors offer an elegant approach to meeting these requirements. But what are interceptors?
Definition of interceptors: Interceptors are mechanisms that make it possible to intercept and influence actions before or after methods are called. They act as a kind of middleware between the caller and the target function. This approach opens up a wide range of possibilities for improving code quality and flexibility.
Why are interceptors important? The use of interceptors has several advantages. Separation of concerns allows developers to better structure code and create reusable components. In addition, interceptors enable the addition of functionalities such as logging, security and error handling without direct modification of the main code.
Example:
A simple logging interceptor in C#: Let’s create a simple interceptor for logging. For this we use the DynamicProxy which we
install via the Nuget: Autofac.Extras.DynamicProxy.
Now we create our first interceptor and implement the IIntercaptor interface from the previously installed Nuget:
using Castle.DynamicProxy;
namespace ConsoleApp1.Interceptor;
public class LogginInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
Console.WriteLine($"Intercepted: {invocation.Method}");
invocation.Proceed();
}
}
This interceptor enables the integration of logging functions before each method is called.
In this context, a text message is displayed in the console.
To intercept a method call, it’s necessary to declare the corresponding methods as virtual.
Non-virtual methods cannot be overwritten and therefore cannot be intercepted.
namespace ConsoleApp1.Classes;
public class Methods
{
public virtual void MethodA()
{
Console.WriteLine("method A and intercepted!!");
}
public void MethodB()
{
Console.WriteLine("method B, not virtual => no interception! :(");
}
}
When starting the application, the creation of a proxy object for the “Methods” class enables the ability to intercept and influence certain method calls, while others are not intercepted.
see the advantage:
it would be very easy to retrofit the interceptor!
using Castle.DynamicProxy;
using ConsoleApp1.Classes;
using ConsoleApp1.Interceptor;
var generator = new ProxyGenerator();
var proxy = generator.CreateClassProxy<Methods>(new LogginInterceptor());
// "Intercepting: MethodA"
proxy.MethodA();
// method B is not virtual -> no interception
proxy.MethodB();
The underlying principle should now be clear.
Let us now take a closer look at this example in the context of the Entity Framework:
Use of interceptors with the Entity Framework:
In Entity Framework, it’s possible to intercept and manipulate operations on the database before they are actually executed.
Interceptors can be used effectively in combination with the Entity Framework to monitor, influence and optimize database access.
These interceptors must be registered under EntityFramework when creating the DBContext in order for them to work.
To do this, we must add the interceptors in the configuration of the DBContext
public class DBContext : DbContext
{
public DBContext(DbContextOptions<DBContext> options)
: base(options)
{
this.EnsureSeedData(); // new line added
}
//add the interceptors into the configbuilder of the dbcontext
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.AddInterceptors(
new EfLoggingInterceptor(), // Loggin interceptor
new EfTransactionInterceptor(), // Transaction interceptor
new EfSaveChangesInterceptor()); // saveChanges interceptor
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(Employee).Assembly);
}
public DbSet<Employee> Data { get; set; }
}
Here is an example of a simple logging interceptor for the Entity Framework:
using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Data.Common;
public class EfLoggingInterceptor : DbCommandInterceptor
{
public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<int> result)
{
Console.WriteLine($"Executing NonQuery: {command.CommandText}");
return base.NonQueryExecuting(command, eventData, result);
}
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
Console.WriteLine($"Executing Reader: {command.CommandText}");
return base.ReaderExecuting(command, eventData, result);
}
// Additional methods for various actions such as ScalarExecuting, ScalarExecuted, etc.
}
This class extends the DbCommandInterceptor class of the Entity Framework and enables the monitoring of database commands.
Best practices for the use of interceptors with EF:
- Targeted application: Identify specific scenarios where interceptors offer the greatest benefit to avoid unnecessary complexity.
- Performance considerations: Be careful with large-scale or computationally intensive operations in interceptors to avoid impacting performance.
- Error handling: Implement appropriate error management to ensure that interceptors are robust and error-free
Example application: Interception query
In the example, we integrate a logging interceptor into a C# application with the Entity Framework. This logs database operations in order to provide an insight into the executed SQL commands.
To implement the EfLoggingInterceptor, we first need the derivation of the base class DbCommandInterceptor. In our scenario, we want to intercept a database query and analyze what data the user has queried. It is important to note that manipulating the query result can lead to possible errors. To be able to access the data anyway, we use a “special trick”.
It is advisable to note that interceptors under Entity Framework should ideally not perform extensive operations, as this can lead to potential problems such as performance degradation or errors that may occur when processing results.
The ReaderExecuted method has been overridden to intercept and analyze the result of a query.
Pay attention to the comments in this example!
public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result)
{
if (eventData.Context == null)
{
return base.ReaderExecuted(command, eventData, result);
}
/// In this example, we intercept the result that the user expects from the database and look at what the user has queried.
/// Here we have to make sure that we do not materialize the result object, otherwise it will be unusable for the user and trigger an error.
/// therefore we create a copy of the result and load it into a DataTable,
/// then read it out.
using var dt = new DataTable();
dt.Load(result);
if (eventData.CommandSource == CommandSource.LinqQuery)
{
foreach (var row in dt.Rows.Cast<DataRow>())
{
string Id = row["Id"].ToString();
string FirstName = row["FirstName"].ToString();
string LastName = row["LastName"].ToString();
LogInfo("EFCommandInterceptor.ReaderExecuted", $"{ Id} >> { FirstName}, { LastName}", command.CommandText);
}
}
//since the user is still waiting for his result, we create a new reader here and then return it
return base.ReaderExecuted(command, eventData, dt.CreateDataReader());
}
It should be noted that it is comparatively straightforward to intercept and analyze database queries and then forward them to the user.
Manipulation interception
Intercepting insert, delete or update commands in Entity Framework can also be done using interceptors. Similar to query interception, you can override the corresponding methods in a DbCommandInterceptor to monitor these changes.
Here is an example of how you can create an interceptor for insert, delete and update commands:
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
DbContext? dbContext = eventData.Context;
if (dbContext == null)
{
return base.SavingChanges(eventData, result);
}
/// We get our entities via the ChangeTracker.
/// However, we only want to intercept those that we have previously defined,
/// here I have used a simple interface to identify these entities:
var entities = dbContext!.ChangeTracker.Entries<IWiretap>().ToList();
foreach (var entry in entities)
{
var auditMessage = entry.State switch
{
EntityState.Deleted => CreateDeleted(entry),
EntityState.Modified => CreateModified(entry),
EntityState.Added => CreateAdded(entry),
_ => null
};
if (auditMessage != null)
{
Console.WriteLine(auditMessage);
}
}
return base.SavingChanges(eventData, result);
}
A notification is generated and displayed for each manipulation operation.
Attentive observers will have noticed that before the data is saved in the database, it is intercepted in order to output the message.
Here you can influence the point in time at which the interceptor should intercept.
Let’s take a look at the different types of interceptors under Entity Framework:
Query Interception:
- BeforeQueryExecuted: Allows database queries to be intercepted before they are sent to the database. This offers the possibility to check or change queries before they are executed.
SaveChanges Interception:
- BeforeSave: Allows custom code to be executed before saving changes to the database. This can be useful to check or validate changes before they are written to the database.
- AfterSave: Allows custom code to be executed after changes have been saved to the database.
- ThrowingConCurrencyException: Allows custom code to be executed before a ConcurrencyException is thrown.
A concurrency error is an error that occurs when multiple threads or processes access or modify shared resources simultaneously without proper synchronization or coordination. This can lead to inconsistent, unexpected or incorrect results that are difficult to reproduce or fix.
a classic example of concurrency is when two users want to change a value in a table at the same time.
Transaction Interception:
AfterCommit: Allows code to be executed after a transaction has been successfully completed.
AfterRollback: Allows code to be executed after a transaction has been rolled back.
Connection Interception:
- **Opening: **Enables the interception of the opening of a database connection.
- Closing: Enables the closing of a database connection to be intercepted.
Advantages of interceptions in Entity Framework:
user-defined logic:
Benefit: Developers can integrate custom logic before or after database operations. This allows finer control over data access behavior.
security checks:
Benefit: Interceptions allow the addition of security checks before executing database queries or saving changes to prevent unauthorized access.
logging:
Benefit: Developers can use interceptions to log SQL commands, which is helpful for debugging and performance monitoring.
data validation:
Advantage: Before saving changes, developers can implement custom validations to ensure that only valid data is written to the database.
transaction management:
Advantage: Interceptions provide the ability to implement custom transaction logic, including code that executes after a transaction is committed or rolled back.
Disadvantages of Interceptions in Entity Framework:
Increase complexity:
Cons: Using interceptions can increase code complexity, especially if they are not carefully managed. Too many interceptions can lead to code that is difficult to understand.
Performance considerations:
Disadvantage: Insufficiently optimized code in the interception methods can affect performance. Developers must ensure that the added logics are efficient.
Dependency on Entity Framework version:
Cons: Some interception functions may depend on the version of Entity Framework, which can lead to compatibility issues when an application is upgraded to a newer version.
Restrictions in certain scenarios:
Cons: In some complex scenarios, interceptions might not fulfill all requirements. Developers need to make sure that interceptions are suitable for their specific application.
Considering these pros and cons, it is crucial to use interceptions in Entity Framework with caution so as not to unnecessarily impact code maintainability and performance.
Top comments (0)