Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/Weasel.EntityFrameworkCore.Tests/JsonColumnEntities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Weasel.EntityFrameworkCore.Tests;

public class EntityWithJsonColumn
{
public Guid Id { get; set; }
public string InternalName { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public ExtendedProperties ExtendedProperties { get; set; } = new();
}

public class ExtendedProperties
{
public string? Theme { get; set; }
public string? Language { get; set; }
public int MaxItems { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore;

namespace Weasel.EntityFrameworkCore.Tests.Postgresql;

public class JsonColumnDbContext : DbContext
{
public const string ConnectionString = "Host=localhost;Port=5432;Database=marten_testing;Username=postgres;Password=postgres";

public JsonColumnDbContext(DbContextOptions<JsonColumnDbContext> options) : base(options)
{
}

public DbSet<EntityWithJsonColumn> Entities => Set<EntityWithJsonColumn>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<EntityWithJsonColumn>(entity =>
{
entity.ToTable("entities", "ef_json_test");
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).HasColumnName("id");
entity.Property(e => e.InternalName).HasColumnName("internal_name");
entity.Property(e => e.Name).HasColumnName("name").HasMaxLength(100);
entity.Property(e => e.Description).HasColumnName("description").HasMaxLength(2000);
entity.OwnsOne(e => e.ExtendedProperties, b =>
{
b.ToJson("extended_properties");
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Shouldly;
using Weasel.Core;
using Weasel.Postgresql;
using Xunit;

namespace Weasel.EntityFrameworkCore.Tests.Postgresql;

/// <summary>
/// Tests that EF Core entities with OwnsOne().ToJson() mappings
/// produce a JSON column in the Weasel table definition.
/// See https://github.com/JasperFx/weasel/issues/232
/// </summary>
public class json_column_end_to_end : IAsyncLifetime
{
private IHost _host = null!;

public async Task InitializeAsync()
{
_host = Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
services.AddDbContext<JsonColumnDbContext>(options =>
options.UseNpgsql(JsonColumnDbContext.ConnectionString));

services.AddSingleton<Migrator, PostgresqlMigrator>();
})
.Build();

await _host.StartAsync();
}

public async Task DisposeAsync()
{
await _host.StopAsync();
_host.Dispose();
}

[Fact]
public void should_map_json_column_from_owned_entity_with_to_json()
{
using var scope = _host.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<JsonColumnDbContext>();
var migrator = scope.ServiceProvider.GetRequiredService<Migrator>();

var entityType = context.Model.FindEntityType(typeof(EntityWithJsonColumn));
entityType.ShouldNotBeNull();

var table = migrator.MapToTable(entityType);

table.ShouldNotBeNull();
table.Identifier.Name.ShouldBe("entities");

// Scalar columns should be mapped
table.HasColumn("id").ShouldBeTrue();
table.HasColumn("internal_name").ShouldBeTrue();
table.HasColumn("name").ShouldBeTrue();
table.HasColumn("description").ShouldBeTrue();

// The JSON column from OwnsOne().ToJson() should be mapped
table.HasColumn("extended_properties").ShouldBeTrue("The JSON column 'extended_properties' should be mapped from OwnsOne().ToJson()");
}

[Fact]
public void json_column_should_be_jsonb_type()
{
using var scope = _host.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<JsonColumnDbContext>();
var migrator = scope.ServiceProvider.GetRequiredService<Migrator>();

var entityType = context.Model.FindEntityType(typeof(EntityWithJsonColumn));
entityType.ShouldNotBeNull();

var table = migrator.MapToTable(entityType);

// The JSON column should exist and have the correct type
table.HasColumn("extended_properties").ShouldBeTrue();
var pgTable = table.ShouldBeOfType<Weasel.Postgresql.Tables.Table>();
var column = pgTable.ColumnFor("extended_properties");
column.ShouldNotBeNull();
column.Type.ShouldBe("jsonb");
}

[Fact]
public async Task can_apply_migration_with_json_column()
{
using var scope = _host.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<JsonColumnDbContext>();

// Clean up any previous test schema
await context.Database.ExecuteSqlRawAsync("DROP SCHEMA IF EXISTS ef_json_test CASCADE");

// Use Weasel to create migration and apply it
var migration = await _host.Services.CreateMigrationAsync(context, CancellationToken.None);
migration.ShouldNotBeNull();

await migration.ExecuteAsync(JasperFx.AutoCreate.CreateOrUpdate, CancellationToken.None);

// Verify data round-trip with JSON column
var entity = new EntityWithJsonColumn
{
Id = Guid.NewGuid(),
InternalName = "test-entity",
Name = "Test Entity",
Description = "A test entity with JSON properties",
ExtendedProperties = new ExtendedProperties
{
Theme = "dark",
Language = "en",
MaxItems = 50
}
};

context.Entities.Add(entity);
await context.SaveChangesAsync();

var retrieved = await context.Entities.FindAsync(entity.Id);
retrieved.ShouldNotBeNull();
retrieved.ExtendedProperties.Theme.ShouldBe("dark");
retrieved.ExtendedProperties.Language.ShouldBe("en");
retrieved.ExtendedProperties.MaxItems.ShouldBe(50);

// Clean up
await context.Database.ExecuteSqlRawAsync("DROP SCHEMA IF EXISTS ef_json_test CASCADE");
}
}
19 changes: 19 additions & 0 deletions src/Weasel.EntityFrameworkCore/DbContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,9 @@ public static ITable MapToTable(this Migrator migrator, IEntityType entityType)
mapColumn(property, storeObjectIdentifier, primaryKeyPropertyNames, table);
}
}

// Add JSON columns from owned entities mapped via OwnsOne().ToJson()
mapJsonColumns(et, addedColumns, table);
}

// Set primary key constraint name from EF Core metadata.
Expand Down Expand Up @@ -462,6 +465,22 @@ private static void mapColumn(IProperty property, StoreObjectIdentifier storeObj
}
}

private static void mapJsonColumns(IEntityType entityType, HashSet<string> addedColumns, ITable table)
{
foreach (var navigation in entityType.GetNavigations())
{
var targetType = navigation.TargetEntityType;
if (!targetType.IsMappedToJson()) continue;

var columnName = targetType.GetContainerColumnName();
if (columnName == null || !addedColumns.Add(columnName)) continue;

var columnType = targetType.GetContainerColumnType() ?? "jsonb";
var column = table.AddColumn(columnName, columnType);
column.AllowNulls = !navigation.ForeignKey.IsRequired;
}
}

private static CascadeAction mapDeleteBehavior(DeleteBehavior deleteBehavior)
{
return deleteBehavior switch
Expand Down
Loading