DEV Community

Cover image for Using JSON data types with .NET EF Core
Aatif G.
Aatif G.

Posted on • Edited on

Using JSON data types with .NET EF Core

Since v7, Entity Framework has provided support for JSON data types in the database, it means we can map JSON to classes and query JSON fields in our Linq queries.
However PostgreSQL has been supporting column type: json since more than a decade, they even have another variant, column type: jsonb.

Here we will not go into details about the difference between json and jsonb: just keep in mind that json is stored as is in string format preserving the fields order and white spaces, but jsonb is stored in binary and it also allows indexing on json fields.

Table showing json and jsonb type columns

Let's create a small .Net Web API project using PostgreSQL at the backend with some json and jsonb types.
Before starting, take a look at the project dependencies:

nuget packages and their versions

First of all, we will create a table in our PostgreSQL db, it will have two special fields, of type json and jsonb.

CREATE TABLE IF NOT EXISTS public."user"
(
    "createdAt" timestamp without time zone NOT NULL DEFAULT now(),
    fullname character varying COLLATE pg_catalog."default",
    email character varying COLLATE pg_catalog."default",
    address json,
    addressb jsonb,
    id bigint NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 )
)

TABLESPACE pg_default;

ALTER TABLE IF EXISTS public."user"
    OWNER to postgres;
Enter fullscreen mode Exit fullscreen mode

Now define the EF model class with sub elements (JSON objects).

public class User : BaseEntity
{
    public string fullname { get; set; }
    public string email { get; set; }
    public Address address { get; set; }
    public Address addressb { get; set; }     
}
public class Address
{
    public string street { get; set; }
    public string city { get; set; }
    public string zipcode { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

For database operations, we will use inheritance to avoid repeated code, basic CRUD operations will be done in a parent service class (GenericService) using parent model class and an interface.
(You can find the base model class/interface in the project folder)

public class DBContext : DbContext
{
    public DbSet<User> user  { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseNpgsql("Connection_String");

}//class
Enter fullscreen mode Exit fullscreen mode

Let's create a new class for database operations, inheriting the parent class.

public class GenericService<Entity> where Entity : class, IBaseEntity
{
    private readonly DBContext _context;

    public GenericService(DBContext dbContext)
    {
        _context = dbContext;
    }

    //Delete the entity by id
    public async Task<bool> Delete(long id)
    {
        try
        {
             var entity = await GetById(id);
             _context.Set<Entity>().Remove(entity);
             var deleteCount = await _context.SaveChangesAsync();

             return deleteCount > 0 ? true : false;
        }
        catch (Exception ex)
        { 
             throw ex; 
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

UserService class will have the methods called by the Controller like GET, PUT, DELETE etc.

public class UserService : GenericService<User>
{
    private readonly DBContext dbcontext;

    public UserService(DBContext dbcontext) : base(dbcontext)
    {
        this.dbcontext = dbcontext;
    }

    public async Task<bool> DeleteUserById(long userId)
    {
        try
        {
            return await Delete(userId);
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

You will notice that we have not done anything specific for the JSON types in the context class yet. We will define the JSON mapping in OnModelCreating method of the context.

modelBuilder.Entity<User>().OwnsOne(o => o.address, sa =>
{
    sa.ToJson(); // Maps address to a JSON column
});

modelBuilder.Entity<User>().OwnsOne(o => o.addressb, sa =>
{
    sa.ToJson(); // Maps addressb to a JSON column
});
Enter fullscreen mode Exit fullscreen mode

This will allow us to query json fields e.g. zipcode in the address object in our EF code like this:

dbcontext.user.Where(x => x.addressb.zipcode == zip)

We will also follow the same nested object pattern to poplate User object to update or create record in DB.

User? newUser = new();

newUser.addressb = new Address
{
    street = street,
    city = city,
    zipcode = zip
};

newUser.address = new Address
{
    street = street,
    city = city,
    zipcode = zip
};
newUser.email = email;
newUser.fullname = fullname;
newUser.createdAt = DateTime.Now.ToUniversalTime();

await Update(newUser); //Generic method in Parent class
Enter fullscreen mode Exit fullscreen mode

Dependency Injection

We have to define our Context and Service class in Program.cs

builder.Services.AddTransient<DBContext>();
builder.Services.AddTransient<UserService>();

I have added some endpoints to the controller for basic CRUD operations on the User table.

API Endpoints

Before closing, here are some observations made while buidling the projest:

  • The entity / db table must have a primary key.
  • In some cases, you can also use annotations with type, then you may not need to add .OwnsOne(..) in the Context.
[Column(TypeName = "jsonb")]
public string address { get; set; }
Enter fullscreen mode Exit fullscreen mode

That is it, I hope it will help someone. The project can be downloaded from here.

Bye!

Top comments (0)