Tuesday, December 29, 2020

EF Core 5.0: SaveChanges Events and Interceptors

In this post let's have a look at one of the new features that got introduced with EF Core 5.0. That is SaveChanges Events and Interceptors.

Events

Following events has been introduced,

  • DbContext.SavingChanges
    • This event is raised at the start of SaveChanges or SaveChangesAsync
  • DbContext.SavedChanges
    • This event is raised at the end of a successful SaveChanges or SaveChangesAsync
  • DbContext.SaveChangesFailed
    • This event is raised at the end of a failed SaveChanges or SaveChangesAsync
static async Task Main(string[] args)
{
    using var context = new MyDbContext();
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.SavingChanges += Context_SavingChanges;
    context.SavedChanges += Context_SavedChanges;
    context.SaveChangesFailed += Context_SaveChangesFailed;

    var john = new Student { Name = "John" };
    var jane = new Student { Name = "Jane" };

    context.AddRange(john, jane);
    await context.SaveChangesAsync();
}

private static void Context_SavingChanges(object sender, SavingChangesEventArgs args)
{
    Console.WriteLine($"Starting saving changes.");
}

private static void Context_SavedChanges(object sender, SavedChangesEventArgs args)
{
    Console.WriteLine($"Saved {args.EntitiesSavedCount} No of changes.");
}

private static void Context_SaveChangesFailed(object sender, SaveChangesFailedEventArgs args)
{
    Console.WriteLine($"Saving failed due to {args.Exception}.");
}

Here note that the event sender is always the DbContext instance.

Interceptors

Interceptors can be used for interception (as the name suggests of course), modification, and most importantly Interceptors allow SaveChanges to be suppressed unlike Events. It also provides async counterparts of methods so you can do async actions.

Interceptors needs to implement ISaveChangesInterceptor interface. It contains following method definitions.

  • SavingChanges
    • Called at the start of DbContext.SaveChanges
  • SavingChangesAsync
    • Called at the start of DbContext.SaveChangesAsync.
  • SavedChanges
    • Called at the end of DbContext.SaveChanges.
  • SavedChangesAsync
    • Called at the end of DbContext.SaveChangesAsync.
  • SaveChangesFailed
    • Called when there is an exception thrown from DbContext.SaveChanges.
  • SaveChangesFailedAsync
    • Called when there is an exception thrown from DbContext.SaveChangesAsync.

There is also a SaveChangesInterceptor abstract class included in the framework, so for your Interceptors you can derive from that and override only the required methods, rather than implementing all the methods in the ISaveChangesInterceptor.

So let's see some code. I can create a custom Interceptor like this.

public class MyCustomInterceptor : SaveChangesInterceptor
{

}

Then I need to tell EF to use this Interceptor.

public class MyDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer($@"Data Source=RAVANA-TPP50\MSSQLSERVER2017; Initial Catalog=EfCore5;User ID=sa;Password=sa")
            .AddInterceptors(new MyCustomInterceptor());
    }

    // Some Code
}

Now I can start customizing my Interceptor. For example, I am going to override SavingChangesAsync and SavedChangesAsync.

public class MyCustomInterceptor : SaveChangesInterceptor
{
    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        Console.WriteLine($"Started saving changes.");

        return new ValueTask<InterceptionResult<int>>(result);
    }

    public override ValueTask<int> SavedChangesAsync(SaveChangesCompletedEventData eventData,
        int result,
        CancellationToken cancellationToken = default)
    {
        Console.WriteLine($"Saved {result} No of changes.");

        return new ValueTask<int>(result);
    }
}

Now if I add a record and do SaveChangesAsync(), I can see following output.

Started saving changes.
Saved 1 No of changes.

I have mentioned above, with Interceptors we can suppress the operations.

public class MyCustomInterceptor : SaveChangesInterceptor
{
    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        Console.WriteLine($"Started saving changes.");

        result = InterceptionResult<int>.SuppressWithResult(0);

        return new ValueTask<InterceptionResult<int>>(result);
    }

    public override ValueTask<int> SavedChangesAsync(SaveChangesCompletedEventData eventData,
        int result,
        CancellationToken cancellationToken = default)
    {
        Console.WriteLine($"Saved {result} No of changes.");

        return new ValueTask<int>(result);
    }
}

Here on SavingChangesAsync, I have Suppressed the operation. And now If I try SaveChangesAsync, I can see nowthing is getting Saved.

Started saving changes.
Saved 0 No of changes.

There is more to Interceptors like you can Intercept different low-level database operations such as Db Connection Interception, Db Command Interception, Transaction Interception etc. I suggest you read Interceptors for more information.

Hope this helps.

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment