Isaac.

database

EF Core JSON Columns

Store and query JSON data with Entity Framework Core.

By Emem IsaacNovember 23, 20223 min read
#entity framework#json#database#aspnet core#ef core
Share:

A Simple Analogy

JSON columns are like drawers within drawers. Instead of a completely flat cabinet (relational), you group related items in JSON, making queries more flexible and intuitive.


Why JSON Columns?

  • Flexible schema: Store semi-structured data
  • Query-able: Filter and project JSON content
  • Denormalization: Avoid complex joins
  • Performance: Cache related data together
  • Compatibility: Mix relational and document data

Configuration

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    // JSON column
    public Address Address { get; set; }
    public List<Phone> PhoneNumbers { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string ZipCode { get; set; }
}

public class Phone
{
    public string Type { get; set; }
    public string Number { get; set; }
}

// DbContext configuration
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>()
        .Property(u => u.Address)
        .HasColumnType("jsonb");  // PostgreSQL
    
    modelBuilder.Entity<User>()
        .Property(u => u.PhoneNumbers)
        .HasColumnType("json");
}

Querying JSON

// Filter by JSON property
var users = await context.Users
    .Where(u => u.Address.City == "New York")
    .ToListAsync();

// Project JSON property
var cities = await context.Users
    .Select(u => u.Address.City)
    .ToListAsync();

// Filter JSON array
var usersWithHomePhone = await context.Users
    .Where(u => u.PhoneNumbers.Any(p => p.Type == "Home"))
    .ToListAsync();

// Complex queries
var result = await context.Users
    .Where(u => u.Address.City == "NYC" && 
               u.PhoneNumbers.Count > 1)
    .Select(u => new {
        u.Name,
        u.Address.ZipCode,
        HomePhones = u.PhoneNumbers
            .Where(p => p.Type == "Home")
            .ToList()
    })
    .ToListAsync();

JSON Operators (SQL Server)

// Using SQL.Invoke for advanced operators
var query = context.Users
    .Where(u => EF.Functions.JsonExtract(
        u.Address, "$.City") == "New York");

// Or with value conversions
var users = await context.Users
    .FromSqlInterpolated($@"
        SELECT * FROM Users 
        WHERE Address->>'City' = {city}")
    .ToListAsync();

Practical Example

public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    
    // Nested JSON object
    public BillingInfo BillingInfo { get; set; }
    
    // JSON array
    public List<LineItem> Items { get; set; }
}

public class BillingInfo
{
    public Address Address { get; set; }
    public PaymentMethod PaymentMethod { get; set; }
}

public class PaymentMethod
{
    public string Type { get; set; }  // CreditCard, Bank, etc.
    public string LastDigits { get; set; }
}

public class LineItem
{
    public string ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

// Queries
var creditCardOrders = await context.Orders
    .Where(o => o.BillingInfo.PaymentMethod.Type == "CreditCard")
    .ToListAsync();

var expensiveOrders = await context.Orders
    .Where(o => o.Items.Sum(i => i.Price * i.Quantity) > 1000)
    .ToListAsync();

Performance Considerations

// Index JSON column (PostgreSQL)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>()
        .HasIndex("Address->>'City'")
        .HasName("idx_address_city");
    
    // Or on array elements
    modelBuilder.Entity<Order>()
        .HasIndex("Items->'$.ProductId'")
        .HasName("idx_product_ids");
}

// Use computed properties
public class User
{
    public int Id { get; set; }
    public Address Address { get; set; }
    
    [NotMapped]
    public string City => Address?.City;
}

Best Practices

  1. Use for semi-structured: Not for frequently-queried relations
  2. Don't over-normalize: Keep related data together
  3. Index JSON paths: Query performance
  4. Validate on insert: Ensure data shape
  5. Document schema: JSON structure contracts

Related Concepts

  • Document databases (MongoDB)
  • Denormalization strategies
  • JSON schema validation
  • Full-text search

Summary

JSON columns in EF Core provide flexible storage and querying of semi-structured data. Use them to store related objects without complex normalization while maintaining queryability.

Share:

Written by Emem Isaac

Expert Software Engineer with 15+ years of experience building scalable enterprise applications. Specialized in ASP.NET Core, Azure, Docker, and modern web development. Passionate about sharing knowledge and helping developers grow.

Ready to Build Something Amazing?

Let's discuss your project and explore how my expertise can help you achieve your goals. Free consultation available.

💼 Trusted by 50+ companies worldwide | ⚡ Average response time: 24 hours