Part #1: Using Interceptors With Entity Framework Core
.NET 8 has introduced Interceptors. Here’s how to use them to audit your database transactions.
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.
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.
// 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.
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/
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
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:
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.