Tuesday, February 23, 2021

EF Core: Using HiLo Algorithm to Generate IDs

In this post, let's see how we can use Hi/Lo algorithm as the key generation strategy in EF Core. This can be achieved using UseHiLo method.

As always let's go by an example.

Consider the below code.
public class Category
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Product> Products { getset; }
}

public class Product
{
    public int Id { getset; }

    public string Name { getset; }

    public int CategoryId { getset; }
}
And MyDbContext is setup like this.
public class MyDbContext : DbContext
{
    public DbSet<Category> Categories { getset; }

    public DbSet<Product> Products { getset; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer("ConnectionString")
            .LogTo(Console.WriteLineLogLevel.Information);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Category>(builder =>
        {
            builder
                .HasMany(x => x.Products)
                .WithOne()
                .HasForeignKey(x => x.CategoryId);
        });
    }
}
And now I am adding a Category and a Product.
using var context = new MyDbContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

var category = new Category() { Name = "Some Category" };
context.Categories.Add(category);

var product = new Product { Name =
"Some Product", CategoryId = category.Id };
context.Products.Add(product);

await context.SaveChangesAsync()
The SaveChanges here will fail here with the following error: "The INSERT statement conflicted with the FOREIGN KEY constraint "FK_Products_Categories_CategoryId"". The reason is, we are setting the CategoryId of the Product, but it's still 0 because the Category record is still not saved to the database.

In this kind of a scenario, using HiLo algorithm to determine the Id before the record is actually saved in the database is quite handy.

Enabling HiLo is pretty easy. You can either enable it for all the entities or for a specific entity.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // HiLo Id generation For all the entities
    //modelBuilder.UseHiLo();

    modelBuilder.Entity<Category>(builder =>
    {
        builder
            .HasMany(x => x.Products)
            .WithOne()
            .HasForeignKey(x => x.CategoryId);

        // HiLo Id generation only for Category
        builder
            .Property(x => x.Id)
            .UseHiLo("categoryseq");
    });
}
After this change, if you run the example, you are not going to see the previous error, instead, you should see Category now has a valid Id.
Valid CategoryId
If you have a second category added, it will have an Id of value 2.

Here something important to note is if you do UseHilo() for all the entities, the Ids are going to be kind of messed up, if you take the above example, the Id of the Product here will be 2.

Now let's try 2 simultaneous adding. While the control is waiting on SaveChanges, I am starting another instance of the application. 
Another Instance
Here the new category has the Id of value 11.

Now how is this working?

If you add in database migration, you will see EF is creating a Sequence, something like below.
migrationBuilder.CreateSequence(
    name"categoryseq",
    incrementBy: 10);
And once the database is updated, you can see the created sequence here.
Sequences
categoryseq
Now let's imagine so far we have only created the database and no data is inserted.

Now we are creating a new database context and adding a category to context (context.Categories.Add(category)), EF will run a query like this.
SELECT NEXT VALUE FOR [categoryseq]
This will return 1, and the category will be assigned the value of 1 and it has now blocked Ids from 1 to 10. If we add in another category in the same database context, EF won't run the above query again, it will increment the Id by 1 and when we have added 10 categories, EF will run the above query again to get the next value, which will be 11. But imagine while we are adding these 10 categories, some other user creates a new database context and starts adding categories, then that process will also call the above query, so this time he will get 11 (because so far we have only blocked from 1 to 10). And when we call for next value, we will get 21, because that process has now blocked 11 to 20.

Isn't it nice? But there is a kind of downside here. This can cause missing Ids (if you are worried about the order of the Ids), but generally, it shouldn't be an issue.

Hope this helps!

Happy Coding.

Regards,
Jaliya

Wednesday, February 10, 2021

EF Core: Owned Entity Types

In this post let's see what is Owned Entity Types in Entity Framework Core. This is a very nice feature when it comes to DDD.

Owned Entities are entities that can be only appeared on navigation properties of other entity types. Basically, they can't exist without the Owner.  

Let's go by an example. Consider the following two entities.
public class User
{
    public int Id { getset; }

    public string Name { getset; }

    public Address Address { getset; }
}
public class Address
{
    public string Street { getset; }

    public string City { getset; }
}
And my DbContext looks like this.
public class MyDbContext : DbContext
{
    public DbSet<User> Users { getset; }

    //...
}
So here, we can use OwnsOne to configure the Address Owned Entity to the Owner User as below (I am using Fluent API instead of annotations, you can annotations if you prefer, but I prefer Fluent API all the time).
public class UserEntityConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<Userbuilder)
    {
        builder.ToTable($"{nameof(User)}");

        builder.OwnsOne(x => x.Address);
    }
}
And this will create something like this.
dbo.User
If you don't like the default naming convention, you can always override the behavior like below.
public class UserEntityConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<Userbuilder)
    {
        builder.ToTable($"{nameof(User)}");

        builder.OwnsOne(x => x.Address, y =>
        {
            y.Property(y => y.City)
                    .HasColumnName("City");

            y.Property(y => y.Street)
                    .HasColumnName("Street");
        });
    }
}
And this would give something like below.
dbo.User

Now let's see consider the following code to insert some data and retrieval.
using var context = new MyDbContext();

User user = new User
{
    Name = "John Doe",
    Address = new Address { Street = "Some Street1", City = "Some City1" }
};

await context.Users.AddAsync(user);
await context.SaveChangesAsync();
using var anotherContext = new OrderDbContext();

user = await anotherContext.Users.FirstOrDefaultAsync();
Here for selection, EF Core will generate a query as below.
SELECT TOP(1) [u].[Id], [u].[Name], [u].[City], [u].[Street]
FROM [User] AS [u]
So here you should be able to see a very important thing. When we are getting User, we didn't have to explicitly Include Address to load Address details, it's loading its Owned Entities by default. This might not be a good example, since it's in the same table, we will see a clearer example when we are going through OwnsMany later in this post. 

Note: I am using anotherContext here for retrieval, because EF Core will automatically fix-up navigation properties to any other entities that were previously loaded into the context instance. So even if you don't explicitly include the data for a navigation property, the property may still be populated if some or all of the related entities were previously loaded.

Above was kind of a one-to-one relationship. Now let's have a look at one-to-many type of relationship.
public class User
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Address> Addresses { getset; }
}
public class Address
{
    public string Street { getset; }

    public string City { getset; }

    public string Type { getset; }
}
Now I can use OwnsMany as below.
public class UserEntityConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<Userbuilder)
    {
        builder.ToTable($"{nameof(User)}");

        builder.OwnsMany(x => x.Addresses, y =>
        {
            y.ToTable("UserAddress");

            y.Property(y => y.City)
                .HasColumnName("City");

            y.Property(y => y.Street)
                .HasColumnName("Type");
            y.Property(y => y.Street)
                .HasColumnName("Type");
        });
    }
}
And this would give something like this.
dbo.User and dbo.UserAddress
Now let's have a look at below code to insert some data and retrieval.
using var context = new MyDbContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

var user = new User
{
    Name = "John Doe",
    Addresses = new List<Address>
    {
        new Address { Street = "Some Street1", City = "Some City1", Type= "Shipping" },
        new Address { Street = "Some Street2", City = "Some City2", Type= "Mailing" }
    }
};

await context.Users.AddAsync(user);
await context.SaveChangesAsync();

using var anotherContext = new MyDbContext();

user = await anotherContext.Users.FirstOrDefaultAsync();
Here for selection, the generated query would be the following. (you can basically ignore the subquery, that's because I am getting FirstOrDefault),
SELECT [t].[Id], [t].[Name], [u0].[UserId], [u0].[Id], [u0].[City], [u0].[Street], [u0].[Type]
FROM (
    SELECT TOP(1) [u].[Id], [u].[Name]
    FROM [User] AS [u]
) AS [t]
LEFT JOIN [UserAddress] AS [u0] ON [t].[Id] = [u0].[UserId]
ORDER BY [t].[Id], [u0].[UserId], [u0].[Id]
And here also, without doing .Include(x => x.Addresses), the addresses are being returned.

So that's about it. You can read more on Owned Entity Types by going to the below link.
There are some restrictions when it comes to Owned Entities which makes perfect sense.
  • You cannot create a DbSet<T> for an owned type.
  • You cannot call Entity<T>() with an owned type on ModelBuilder.
  • Instances of owned entity types cannot be shared by multiple owners (this is a well-known scenario for value objects that cannot be implemented using owned entity types).
Hope this helps.

Happy Coding.

Regards,
Jaliya