win7user10 / Laraue.EfCoreTriggers

Library to write triggers in C# with EF.Core
MIT License
114 stars 22 forks source link

[WIP] - Null Reference Exception when using inheritance #24

Closed Meberem closed 2 years ago

Meberem commented 2 years ago

I too was experiencing the issues described in #7 and was poking around.

This is an very rough and incomplete PR but does allow EF to create a migration with inheritance. It doesn't however allow the annotations to be persisted for subsequent migrations. I believe Laraue.EfCoreTriggers.Common.TriggerBuilders.Base.Trigger<TTriggerEntity> needs to derive from Microsoft.EntityFrameworkCore.Storage.CoreTypeMapping so it can be serialized but I am not sure how that should work at the moment.

I thought I would post this in case someone else wished to use this as a jumping off point or if I have some time later

win7user10 commented 2 years ago

Hi, @Meberem, Could you please provide a code to reproduce the error in your case?

Meberem commented 2 years ago

Hi @win7user10 I think I have managed to isolate the issue. It appears more to be an issue with Conversion than inheritance but it is possible to demonstrate it. Here is a Minimal API with the different scenarios and the errors they generate:

using Laraue.EfCoreTriggers.Common.Extensions;
using Laraue.EfCoreTriggers.SqlLite.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Newtonsoft.Json;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MyDbContext>(options =>
{
    options
        .UseSqlite("Filename=\"./my.db\"")
        .UseSqlLiteTriggers();
});
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

public class MyObject
{
    public int Id { get; set; }
    public ChildObject Children { get; set; }
}

public class ChildObject
{
    public string Content { get; set; }
}

public class AuditObject
{
    public int Id { get; set; }
    public ChildObject Children { get; set; }
}

class MyDbContext : DbContext
{
    public MyDbContext(DbContextOptions<MyDbContext> options)
        :base(options)
    {
    }

    public DbSet<MyObject> Objects { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        // OnModelCreating_Success(modelBuilder);
        // OnModelCreating_NullValueProperty_1(modelBuilder);
        // OnModelCreating_NullValueProperty_2(modelBuilder);
        OnModelCreating_MustAddAuditObject(modelBuilder);
    }

    private void OnModelCreating_Success(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<AuditObject>(ChildrenAsJson);

        modelBuilder.Entity<MyObject>(builder =>
        {
            ChildrenAsJson(builder);
            AddTrigger(builder);
        });
    }

    private void OnModelCreating_MustAddAuditObject(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MyObject>(builder =>
        {
            ChildrenAsJson(builder);
            AddTrigger(builder);
        });

        modelBuilder.Entity<AuditObject>(ChildrenAsJson);
    }

    private void OnModelCreating_NullValueProperty_1(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MyObject>(builder =>
        {
            AddTrigger(builder);
            ChildrenAsJson(builder);
        });

        modelBuilder.Entity<AuditObject>(ChildrenAsJson);
    }

    private void OnModelCreating_NullValueProperty_2(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<AuditObject>(ChildrenAsJson);

        modelBuilder.Entity<MyObject>(builder =>
        {
            AddTrigger(builder);
            ChildrenAsJson(builder);
        });
    }

    private void AddTrigger(EntityTypeBuilder<MyObject> builder)
    {
        builder
            .AfterInsert(trigger => trigger.Action(action => action.Insert(myObject => new AuditObject
            {
                Id = myObject.Id,
                Children = myObject.Children,
            })));
    }

    private void ChildrenAsJson(EntityTypeBuilder<MyObject> builder)
    {
        builder
            .Property(x => x.Children)
            .HasColumnType("text")
            .HasConversion(
                child => JsonConvert.SerializeObject(child),
                child => JsonConvert.DeserializeObject<ChildObject>(child));
    }

    private void ChildrenAsJson(EntityTypeBuilder<AuditObject> builder)
    {
        builder
            .Property(x => x.Children)
            .HasColumnType("text")
            .HasConversion(
                child => JsonConvert.SerializeObject(child),
                child => JsonConvert.DeserializeObject<ChildObject>(child));
    }
}
win7user10 commented 2 years ago

@Meberem, Hi, thank you for your suggestion to use object annotation instead of converting to string SQL. I used this while made library refactoring. Inheritance support also will be added after the new version will be published.