koenbeuk / EntityFrameworkCore.Triggered

Triggers for EFCore. Respond to changes in your DbContext before and after they are committed to the database.
MIT License
556 stars 29 forks source link

Accessing context.UnmodifiedEntity on entity with ComplexType crashes #192

Open DrPhil opened 9 months ago

DrPhil commented 9 months ago

Accessing UnmodifiedEntity on a type with a ComplexType on it causes an exception. It doesn't matter if the changed value is inside the complextype or directly on the entity: accessing the unmodified value will crash anyway.

Example:

Models.cs

using EntityFrameworkCore.Triggered;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;

namespace TriggeredComplexType;

public class MyContext : DbContext
{
    public DbSet<MyEntity> MyEntities { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        var folder = Environment.SpecialFolder.LocalApplicationData;
        var path = Environment.GetFolderPath(folder);
        options.UseSqlite($"Data Source={Path.Join(path, "complextypes.db")}");

        options.UseTriggers(triggerOptions =>
        {
            triggerOptions.AddTrigger(typeof(MyTrigger));
        });
    }
}

public class MyEntity
{
    public int Id { get; set; }
    public MyComplexType ComplexType { get; set; }
}

[ComplexType]
public class MyComplexType
{
    public string? Name { get; set; }
    public int Age { get; set; }
}

public class MyTrigger : IBeforeSaveTrigger<MyEntity>
{
    public async Task BeforeSave(ITriggerContext<MyEntity> context, CancellationToken cancellationToken)
    {
        Console.WriteLine("Here we are in the trigger");
        Console.WriteLine($"The unmodified entity is: {context.UnmodifiedEntity}");
    }
}

Program.cs

using TriggeredComplexType;

await using var context = new MyContext();

var myEntity = new MyEntity
{
    ComplexType = new MyComplexType
    {
        Name = "John",
        Age = 30
    }
};

Console.WriteLine("Adding to context");
context.Add(myEntity);
await context.SaveChangesAsync();

Console.WriteLine("Updating context");
myEntity.ComplexType.Name = "Jane";
await context.SaveChangesAsync();

csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="EntityFrameworkCore.Triggered" Version="3.2.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.2" />
  </ItemGroup>

</Project>

Output

Adding to context
Here we are in the trigger
The unmodified entity is: 
Updating context
Here we are in the trigger
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at lambda_method50(Closure, MaterializationContext)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ArrayPropertyValues.ToObject()
   at EntityFrameworkCore.Triggered.TriggerContext`1.get_UnmodifiedEntity()
   at TriggeredComplexType.MyTrigger.BeforeSave(ITriggerContext`1 context, CancellationToken cancellationToken) in /home/simon/Github/TriggeredComplexType/Models.cs:line 48
   at EntityFrameworkCore.Triggered.TriggerSession.RaiseTriggers(Type openTriggerType, Exception exception, ITriggerContextDiscoveryStrategy triggerContextDiscoveryStrategy, Func`2 triggerTypeDescriptorFactory, CancellationToken cancellationToken)
   at EntityFrameworkCore.Triggered.Internal.TriggerSessionSaveChangesInterceptor.SavingChangesAsync(DbContextEventData eventData, InterceptionResult`1 result, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Program.<Main>$(String[] args) in /home/simon/Github/TriggeredComplexType/Program.cs:line 24
   at Program.<Main>$(String[] args) in /home/simon/Github/TriggeredComplexType/Program.cs:line 24
   at Program.<Main>(String[] args)
koenbeuk commented 7 months ago

Can you try and use v4.0.0-preview.1? I couldn't reproduce this issue with that version

DrPhil commented 7 months ago

I could reproduce this with 4.0.0-preview.1 as well.

Modified csproj

    <PackageReference Include="EntityFrameworkCore.Triggered" Version="4.0.0-preview.1" />

Modified Models.cs

public class MyTrigger : IBeforeSaveTrigger<MyEntity>
{
    public void BeforeSave(ITriggerContext<MyEntity> context)
    {
        Console.WriteLine("Here we are in the trigger");
        Console.WriteLine($"The unmodified entity is: {context.UnmodifiedEntity}");
    }
}

Output

Adding to context
Here we are in the trigger
The unmodified entity is: 
Updating context
Here we are in the trigger
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at lambda_method50(Closure, MaterializationContext)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ArrayPropertyValues.ToObject()
   at EntityFrameworkCore.Triggered.TriggerContext`1.get_UnmodifiedEntity()
   at TriggeredComplexType.MyTrigger.BeforeSave(ITriggerContext`1 context) in /home/simon/Github/TriggeredComplexType/Models.cs:line 42
   at EntityFrameworkCore.Triggered.Internal.TriggerTypeDescriptorHelpers.<>c__DisplayClass2_0`2.<GetWeakDelegateHelper>b__0(Object trigger, Object triggerContext)
   at EntityFrameworkCore.Triggered.Internal.Descriptors.BeforeSaveTriggerDescriptor.Invoke(Object trigger, Object triggerContext, Exception exception)
   at EntityFrameworkCore.Triggered.Internal.TriggerDescriptor.Invoke(Object triggerContext, Exception exception)
   at EntityFrameworkCore.Triggered.TriggerSession.RaiseTriggers(Type openTriggerType, Exception exception, ITriggerContextDiscoveryStrategy triggerContextDiscoveryStrategy, Func`2 triggerTypeDescriptorFactory)
   at EntityFrameworkCore.Triggered.TriggerSession.RaiseBeforeSaveTriggers(Boolean skipDetectedChanges)
   at EntityFrameworkCore.Triggered.TriggerSession.RaiseBeforeSaveTriggers()
   at EntityFrameworkCore.Triggered.Internal.TriggerSessionSaveChangesInterceptor.SavingChangesAsync(DbContextEventData eventData, InterceptionResult`1 result, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Program.<Main>$(String[] args) in /home/simon/Github/TriggeredComplexType/Program.cs:line 21
   at Program.<Main>$(String[] args) in /home/simon/Github/TriggeredComplexType/Program.cs:line 21
   at Program.<Main>(String[] args)

I tried with the async version as well, and had the same result. I uploaded the example to a repository so hopefully you can reproduce as well. https://github.com/DrPhil/triggered-complextype-crash

stale[bot] commented 5 months ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

DrPhil commented 5 months ago

Is there anything else you need from me? Did my example in the repo reproduce for you?

koenbeuk commented 3 months ago

This issue seems to be resolved when targeting EF Core 9. I assume that there was an issue fixed related to this issue as the stacktrace points to something internal to EF Core. I'll leave this issue open for now as we may want to find a way to prevent this issue from happening when targeting EF Core 8 or below

stale[bot] commented 1 month ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.