Part #1: Using Interceptors With Entity Framework Core

.NET 8 has introduced Interceptors. Here’s how to use them to audit your database transactions.

Nathan
The Tech Collective
6 min readMay 7, 2024

--

In a previous project, our team had an Audit table in the database. Every time a request modified a specific Entity in our app we needed to create an Audit entry to detail the change.

We needed to provide a timestamp, the Type of modification, the Author and some Before and After fields. Our app had many endpoints which implemented a lot of changes. As a result, we were creating and saving Audit entries in various places. Sometimes we created Audit entries in the Controller and sometimes inside the Entity. We didn’t have the luxury of time to refactor so we never settled on a uniform pattern.

There must have been a way for us to create these Audit entries in a single place away from the core business logic.

The answer lies with Interceptors.

Photo by ThisisEngineering on Unsplash

Interceptors allow you to step into an Entity Framework Core operation. Creating a concrete class that inherits from SaveChangesInterceptor can be powerful. SaveChangesInterceptor exposes methods that allow you to modify the saving process.

In our example, we will create an application that stores information on Employees. The app will allow us to Create, Read, Update and Delete Employees. For simplicity, we will use an SQLite database and DB Browser for SQLite to read the data.

Create the app from scratch

Before we start we need to install dotnet tool to allow us to run dotnet commands. Run the following command in your terminal:

dotnet tool install --global dotnet-ef

Run dotnet ef in the command line. If you get a positive response then you have installed it.

Create a new folder named EmployeeInterceptor. In your bash terminal navigate into the folder and enter the following command:

cd EmployeeInterceptor
dotnet new sln -n EmployeeInterceptor

We are now going to create an empty Console App and build out our Minimal API from scratch. Inside the EmployeeInterceptor folder make a new Console App:

// The -n flag sets the name of our Console App
dotnet new console -n EmployeeInterceptor.Api

Lastly, we need to attach this new project to our solution. At the same level as the solution file, enter the following commands:

dotnet sln EmployeeInterceptor.sln add EmployeeInterceptor.Api/EmployeeInterceptor.Api.csproj

You can now launch your text editor and open the newly created project solution.

To turn the Console App into a Web API, open the EmployeeInterceptor.Api.csproj file and append .Web to the Project SDK type.

If you struggle to locate the .csproj file, switch your Solution Explorer to ‘Folder View’ (or ‘File System’). There you will be able to view all of your files.

Switching to File System view
// EmployeeInterceptor.Api.csproj

<Project Sdk="Microsoft.NET.Sdk">
//
</Project>

// --> change into -->

<Project Sdk="Microsoft.NET.Sdk.Web"> <-- here
//
</Project>

Remove all of the code from Program.cs and add the following:

var builder = WebApplication.CreateBuilder();

// configure your app

var app = builder.Build();

// build your app

app.Run();

We now have everything we need to start our Minimal API.

Photo by ThisisEngineering on Unsplash

Setup Entity Framework Core & SQLite

To begin we need to install the required Nuget packages for Entity Framework Core. Again, in the command line run the following commands. Make sure you are at the same level of the project as your *.csproj file

cd EmployeeInterceptor.Api/

After installing Entity Framework Core and SQLite we now set up a connection string.

Create a new file at the root of your EmployeeInterceptor.Api project called appsettings.json. In your appsettings.json file add the below config:

{
"ConnectionStrings": {
"DefaultConnection": "Data Source=sqlite.db"
}
}

Unlike most other SQL databases, SQLite doesn’t use a separate server process. SQLite reads and writes to a file on your computer.

Adding your data source as we did in the above code will save a new .db file to the project’s root. The file’s name will be sqlite.db. Later on, we will use DB Browser for SQLite to view the data.

Next, we want to create our Entities. Add two new folders to the root of your project: Interfaces and Entities. Inside Entities create a new file named Employee then fill it with the following fields:

public class Employee
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Department { get; set; }
public string JobTitle { get; set; }
}

Then inside Interfaces create a new interface called IAuditable. For this demo the class will remain empty but I wanted to introduce the idea regardless.

IAuditable will help our Interceptor know which Entities to track. An alternative option would be to add auditing fields in this file. We could then expose them to an Entity through inheritance. Rather than every Entity having a CreatedAt field we store it in the interface instead. Reducing code in the process.

public interface IAuditable
{

}

Then have Employee inherit IAuditable. Import any using statements to help the code compile:

public class Employee : IAuditable
{
public int Id { get; set; }
//
}

For the Entity Framework to work our app needs a DbContext file. DbContext maps the entities and relationships defined in our app to tables in a database.

Add a new folder at the project’s root and call it Data. Inside add a new file called DataContext which will inherit from DbContext:

public class DataContext : DbContext
{
public DataContext(DbContextOptions<DataContext> options) : base(options)
{

}
}

Our DbContext must expose a public constructor that takes DbContextOptions as an argument. The DbContextOptions carries the configuration information needed to configure the DbContext. We will see why this is helpful when we register our DbContext in Program.cs.

As mentioned earlier, the DbContext maps our entities to tables in a database. We achieve this through using a DbSet and passing in our Entity. Add the Employee entity to the database with the below code:

public class DataContext : DbContext
{
public DataContext(DbContextOptions<DataContext> options) : base(options)
{

}

public DbSet<Employee> Employees { get; set; }
}

Return to Program.cs and register the DbContext. We will tell the DbContext to use SQLite and point it toward our connection string. Our public constructor from before allows us to make these modifications.

var builder = WebApplication.CreateBuilder();

// configure your app

builder.Services.AddDbContext<DataContext>(
options => options.UseSqlite(
builder.Configuration.GetConnectionString("DefaultConnection")));

var app = builder.Build();

//

After creating our entity and registering our DbContext we now create a Migration. Make sure you are at the root of the project and enter the following into the command line:

dotnet ef migrations add InitialMigration

A Migrations folder will now appear in your app. Inside this folder, we can review the plan for this migration. In the Migration file, we can see an Up and a Down method. The Up method details what we intend to create. The Down method represents what will happen if we perform a rollback.

Now let’s update our database using the below command. It will also create the database if it doesn’t exist:

dotnet ef database update

To view the newly created database we can use DB Browser for SQLite. You can install DB Browser for SQLite here. Once installed open DB Browser for SQLite select ‘Open Database’ at the top left and locate your database file. Your database file will live within your project folder. When opened select ‘Browse Data’. You should now be able to see your Employees database.

The project should look like this:

Screenshot of project architecture

Finally, we need to make sure that our app launches in Development mode. Create a new folder called Properties and add a file inside called launchSettings.json. Add the following boilerplate code into the file:

{
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"ProjectName": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

Part #1 has shown us how to create a new .NET Minimal API from scratch. We took this further and created a new database using SQLite and Entity Framework Core. Along with our new Employee entity, we have everything we need to move on and build out our endpoints.

In the next part of our series, we will install Swagger and create our CRUD operations.

--

--