Skip to content

EF Core Integration

Ahmad Al-freihat edited this page Jan 1, 2026 · 1 revision

EF Core Integration

The Masterly.NonEmptyList.EntityFrameworkCore package provides Entity Framework Core integration for NonEmptyList<T> and ImmutableNonEmptyList<T>.

Installation

dotnet add package Masterly.NonEmptyList.EntityFrameworkCore

Or via Package Manager Console:

Install-Package Masterly.NonEmptyList.EntityFrameworkCore

Value Conversion (JSON Storage)

Store NonEmptyList<T> properties as JSON strings in the database.

Configuration

using Masterly.NonEmptyList.EntityFrameworkCore;

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public NonEmptyList<string> Tags { get; set; } = new("default");
}

public class AppDbContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>()
            .Property(p => p.Tags)
            .HasNonEmptyListConversion();
    }
}

Custom JSON Options

var options = new JsonSerializerOptions { WriteIndented = true };

modelBuilder.Entity<Product>()
    .Property(p => p.Tags)
    .HasNonEmptyListConversion(options);

ImmutableNonEmptyList

public class Document
{
    public int Id { get; set; }
    public ImmutableNonEmptyList<string> Authors { get; set; }
}

modelBuilder.Entity<Document>()
    .Property(d => d.Authors)
    .HasImmutableNonEmptyListConversion();

Auto-Configuration (Conventions)

Automatically configure all NonEmptyList<T> properties:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    // Auto-configure all NonEmptyList and ImmutableNonEmptyList properties
    modelBuilder.ApplyNonEmptyListConventions();
}

One-to-Many Relationships

Configure one-to-many relationships with NonEmptyList<T> as the navigation property.

Basic Relationship

public class Order
{
    public int Id { get; set; }
    public string OrderNumber { get; set; }
    public NonEmptyList<OrderItem> Items { get; set; } = null!;
}

public class OrderItem
{
    public int Id { get; set; }
    public string ProductName { get; set; }
    public int OrderId { get; set; }
    public Order Order { get; set; }
}

Configuration

modelBuilder.Entity<Order>()
    .HasNonEmptyManyWithOne(
        o => o.Items,
        i => i.Order,
        i => i.OrderId);

With Delete Behavior

modelBuilder.Entity<Order>()
    .HasNonEmptyManyWithDeleteBehavior(
        o => o.Items,
        i => i.Order,
        DeleteBehavior.Cascade);

Required Navigation

modelBuilder.Entity<Order>()
    .HasRequiredNonEmptyMany(
        o => o.Items,
        i => i.Order);

Many-to-Many Relationships

Configure many-to-many relationships with NonEmptyList<T>.

Basic Many-to-Many

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public NonEmptyList<Course> Courses { get; set; } = null!;
}

public class Course
{
    public int Id { get; set; }
    public string Title { get; set; }
    public NonEmptyList<Student> Students { get; set; } = null!;
}
modelBuilder.Entity<Student>()
    .HasNonEmptyManyToMany(
        s => s.Courses,
        c => c.Students);

With Explicit Join Entity

public class AuthorBook
{
    public int AuthorId { get; set; }
    public int BookId { get; set; }
    public DateTime AssignedDate { get; set; }
}

modelBuilder.Entity<Author>()
    .HasMany(a => a.Books)
    .WithMany(b => b.Authors)
    .UsingEntity<AuthorBook>(
        l => l.HasOne<Book>().WithMany().HasForeignKey(ab => ab.BookId),
        r => r.HasOne<Author>().WithMany().HasForeignKey(ab => ab.AuthorId));

Important Limitation

Include() does not work with NonEmptyList<T> navigation properties. EF Core cannot instantiate NonEmptyList<T> during materialization because it requires at least one element.

Use projections instead:

// DON'T DO THIS - will throw
var student = context.Students.Include(s => s.Courses).First();

// DO THIS - use projection
var result = context.Students
    .Where(s => s.Id == studentId)
    .Select(s => new
    {
        s.Name,
        CourseCount = s.Courses.Count(),
        CourseTitles = s.Courses.Select(c => c.Title).ToList()
    })
    .FirstOrDefault();

Validation Interceptor

Validate NonEmptyList<T> properties on save:

services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString)
           .UseNonEmptyListValidation();
});

This throws InvalidOperationException if any NonEmptyList<T> property is null or empty when saving.

Query Extensions

Convert query results to NonEmptyList<T>:

// ToNonEmptyList - throws if empty
NonEmptyList<Product> products = context.Products
    .Where(p => p.IsActive)
    .ToNonEmptyList();

// ToNonEmptyListOrNull - returns null if empty
NonEmptyList<Product>? products = context.Products
    .Where(p => p.Category == "Electronics")
    .ToNonEmptyListOrNull();

// Async versions
NonEmptyList<Product> products = await context.Products
    .Where(p => p.Price > 100)
    .ToNonEmptyListAsync();

Complete Example

public class AppDbContext : DbContext
{
    public DbSet<Product> Products => Set<Product>();
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<OrderItem> OrderItems => Set<OrderItem>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options.UseSqlServer(connectionString)
               .UseNonEmptyListValidation();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // JSON conversion for Tags
        modelBuilder.Entity<Product>()
            .Property(p => p.Tags)
            .HasNonEmptyListConversion();

        // One-to-many relationship
        modelBuilder.Entity<Order>()
            .HasNonEmptyManyWithOne(
                o => o.Items,
                i => i.Order,
                i => i.OrderId);
    }
}

Supported Features Summary

Feature Supported
JSON Value Conversion ✅ Yes
ImmutableNonEmptyList ✅ Yes
One-to-Many Relationships ✅ Yes
Many-to-Many Relationships ✅ Yes (with limitations)
Include() on NonEmptyList ❌ No (use projections)
Query Extensions ✅ Yes
Validation Interceptor ✅ Yes
Change Tracking ✅ Yes
Auto-Configuration ✅ Yes

Next Steps

Clone this wiki locally