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

Tuesday, December 15, 2020

Azure Functions: Change PlanType from Consumption (Serverless) to Premium

This is a quick post on how we can change the Plan Type of an Azure Function App from Consumption (Serverless) to a Premium Plan. If you have created an Azure Function App initially selecting Consumption (Serverless) as the plan type and then later if you want to change it to a Premium Plan, Azure Portal has no direct way to do it.

Here you have 2 options,

  1. Create a new Azure Function App with a Premium plan and redeploy code
  2. Create a new Azure Function Premium Plan and change the existing Function App to use the new plan through Azure CLI
The first option can be a pain, you basically need to setup everything and deploy the code. The second option is pretty easy.

First, we need to create a new Azure Function Premium Plan.
az functionapp plan create `
--resource-group <resource group> `
--name <new plan name> `
--location <location> `
--sku <EP1 or EP2 or EP3> `
--is-linux true
--is-linux is only if you want it to be Linux, well it's self-explanatory.

Next, we need to associate the existing Function App with the new plan.
az functionapp update `
--resource-group <resource group of the existing function app> `
--name <existing function app name> `
--plan <new plan name> `
That's easy. Hope this helps.

Happy Coding.

Regards,
Jaliya

Thursday, December 3, 2020

EF Core 5.0: Many to Many Relationships

One of the nicest features available in EF Core 5.0 is now you can define a many-to-many relationship without explicitly mapping the join table. 

With earlier EF Core versions, you didn't have the luxury of doing something like this,

Example 1:

public class Student
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Course> Courses { getset; }
}

public class Course
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Student> Students { getset; }
}

You needed to explicitly create the join table, something like below.

Example 2:

public class Student
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<CourseStudent> CourseStudents { getset; }
}

public class Course
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<CourseStudent> CourseStudents { getset; }
}

public class CourseStudent
{
    public int Id { getset; }

    public Course Course { getset; }     public Student Student { getset; }
}

But with EF Core 5.0, you don't have to do that anymore, the code in Example 1 would work just fine. EF will understand we need a join table and it will create one automatically.

Database
You can use it like below.
using var context = new MyDbContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

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

var csharp = new Course { Name = "C#", Students = new List<Student> { john, jane } };
var sql = new Course { Name = "SQL", Students = new List<Student> { tim } };

context.AddRange(john, jane, tim, csharp, sql);
await context.SaveChangesAsync();

List<Student> csharpStudents = await context.Students .Where(x => x.Courses.Any(sc => sc.Name == csharp.Name)) .ToListAsync();

foreach (Student student in csharpStudents)
{
    Console.WriteLine(student.Name);
}

Output,

John
Jane

Often times, you need additional data (known as payload properties) in the join table. In our case here, maybe the date that the student got enrolled with the Course. So I can design the data model like below. 

public class Student
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Course> Courses { getset; }

    public ICollection<CourseStudent> CourseStudents { getset; }
}

public class Course
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Student> Students { getset; }

    public ICollection<CourseStudent> CourseStudents { getset; }
}

public class CourseStudent
{
    public Course Course { getset; }

    public Student Student { getset; }

    public DateTime EnrolledDate { getset; }
}

If you have noticed in the above code, you can even specify a navigation property to the Join table. Let's see how it can make our life easy in certain cases.

And EF Core allows full customization of the join table, something like below.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Student>()
        .HasMany(x => x.Courses)
        .WithMany(x => x.Students)
        .UsingEntity<CourseStudent>(
            x => x
                .HasOne(x => x.Course)
                .WithMany(x => x.CourseStudents),
            x => x
                .HasOne(x => x.Student)
                .WithMany(x => x.CourseStudents),
            x =>
            {
                x.Property(x => x.EnrolledDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
            });
}

Then I can do things like this.

using var context = new MyDbContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

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

var csharp = new Course
{
Name = "C#",
    CourseStudents = new List<CourseStudent>
    {
        new CourseStudent
        {
            Student = john,
            EnrolledDate = new DateTime(2020, 06, 01)
        },
        new CourseStudent
        {
            Student = jane,
            EnrolledDate = new DateTime(2020, 10, 01)
        }
    }
};
var sql = new Course { Name = "SQL", Students = new List<Student> { tim } };

context.AddRange(john, jane, tim, csharp, sql);
await context.SaveChangesAsync();

List<Course> courses = await context.Courses
    .ToListAsync();

foreach (Course course in courses)
{
    Console.WriteLine($"{course.Name}");

    // If you don't want payload property information, you can just use Students 
    foreach (Student student in course.Students)
    {
     Console.WriteLine($"-- {student.Name}");
    }

    // If you want payload property information, you can use CourseStudents, the navigation to join table  
    foreach (CourseStudent courseStudent in course.CourseStudents)
    {
        Console.WriteLine($"-- {courseStudent.Student.Name} : {courseStudent.EnrolledDate}");
    }
}

Output,

C#
// Without using Join Table
-- John
-- Jane

// With using Join Table
-- John : 6/1/2020 12:00:00 AM
-- Jane : 10/1/2020 12:00:00 AM

SQL
// Without using Join Table
-- Tim

// With using Join Table
-- Tim : 12/3/2020 9:17:00 PM

Hope this helps.

To know more on EF Core 5, you can read: What's New in EF Core 5.0

Happy Coding.

Regards,
Jaliya