Closed kangzoel closed 2 years ago
Can you clarify what trigger ran twice and on what entity? Mind that modifying a related Entity within a BeforeSave trigger will cause BeforeSave triggers for that related entity to fire again.
This is the trigger I applied
StockInputLogTrigger.cs
internal class StockInputLogTrigger : IBeforeSaveTrigger<StockInputLog>
{
public Task BeforeSave(
ITriggerContext<StockInputLog> context,
CancellationToken cancellationToken
)
{
var stock = context.Entity.Stock;
switch (context.ChangeType)
{
case ChangeType.Added:
stock!.Amount += context.Entity.Amount;
break;
case ChangeType.Deleted:
stock!.Amount -= context.Entity.Amount;
break;
}
return Task.CompletedTask;
}
}
I expect it to make new Stock with the Stock.Amount
equals to the inserted StockInputLogs.Amount
. But instead, it is doubling it. For example, StockInputLogs
with the amount of 10 will result in the Stock.Amount
of 20
It does work normal when doing normal insert though. Not nested insert like the given example above: CreateStockInputWindow.xaml.cs
@kangzoel, The trigger is not supposed to be fired twice for the same entity/ChangeType combo. What is possible is that the trigger is both registered with the application DI container as well as with the Trigger service, e.g.:
services.AddDbContext(options => {
options.UseTriggers(triggerOptions => {
triggerOptions.AddTrigger<StockInputLogTrigger>(); // <-- Here we register the trigger with the trigger service (preferred method).
});
});
services.AddScoped<StockInputLogTrigger>(); // <-- Here we register the trigger with the DI container.
With the above setup, there is a possibility that the trigger will be fired twice for the same entity/ChangeType combo. (I'm planning on stripping support for triggers registered with the DI container completely in the next major release).
If the above does not help: Please try and are a reproducible.
Meanwhile, I trigger can keep state. Registering a trigger with a Scoped lifetime will ensure that it's state is bound to the lifetime of the DbContext instance. You can use this as a work around for your issue, this would look something like:
triggerOptions.AddTrigger<StockInputLogTrigger>(ServiceLifetime.Scoped); // Register your trigger with a scoped lifetime
class StockInputLogTrigger : IBeforeSaveTrigger<StockInputLog>, IBeforeSaveCompletedTrigger {
readonly HashSet<StockInputLog> _processedEntities = new();
public Task BeforeSave(ITriggerContext<StockInputLogTrigger> context, CancellationToken _) {
if (!_processedEntities.Contains(context.Entity)) { // Ensure that we did not process this entity before
_processedEntities.Add(context.Entity); // Record it
// Do your logic
}
}
public Task BeforeSaveCompleted(CacellationToken _) {
_processedEntities.Clear(); // Optional cleanup in case we expect subsequent calls to SaveChanges within the lifetime of this dbcontext
}
}
I don't use dependency injection. I modified my code but it has not effect. The trigger still executed twice. These are my new codes:
DatabaseContext.cs
using System.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using WarehouseApp.Models;
namespace WarehouseApp.Database;
public class DatabaseContext : DbContext
{
public DbSet<Item> Items => Set<Item>();
public DbSet<Stock> Stocks => Set<Stock>();
public DbSet<StockInputLog> StockInputLogs => Set<StockInputLog>();
public DbSet<StockOutputLog> StockOutputLogs => Set<StockOutputLog>();
public DbSet<User> Users => Set<User>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlite("Data Source=database.sqlite")
.EnableSensitiveDataLogging()
.LogTo((e) => Trace.WriteLine(e))
.UseTriggers(triggerOptions =>
{
triggerOptions.AddTrigger<Triggers.StockInputLogTrigger>(ServiceLifetime.Scoped);
});
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Stock>().HasAlternateKey(s => new { s.ItemId, s.ExpirationDate });
}
}
StockInputLogTrigger.cs
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using EntityFrameworkCore.Triggered;
using EntityFrameworkCore.Triggered.Lifecycles;
using WarehouseApp.Models;
namespace WarehouseApp.Triggers;
internal class StockInputLogTrigger : IBeforeSaveTrigger<StockInputLog>, IBeforeSaveCompletedTrigger
{
readonly HashSet<StockInputLog> _processedEntities = new();
public Task BeforeSave(
ITriggerContext<StockInputLog> context,
CancellationToken cancellationToken
)
{
if (!_processedEntities.Contains(context.Entity))
{
_processedEntities.Add(context.Entity);
var stock = context.Entity.Stock;
switch (context.ChangeType)
{
case ChangeType.Added:
stock!.Amount += context.Entity.Amount;
break;
case ChangeType.Deleted:
stock!.Amount -= context.Entity.Amount;
break;
}
}
return Task.CompletedTask;
}
public Task BeforeSaveCompleted(CancellationToken _)
{
_processedEntities.Clear();
return Task.CompletedTask;
}
}
I think I'll use my workaround instead, for the time being, by inserting entities independently (no nesting entity), until the problem is fixed
My bad, the problem is in my code, not the library. I shouldn't have initialize the stock value, and let the StockInputLog trigger handle the initial value. Thus the code should be
CreateStockInputWindow.xaml.cs
...
dbContext.StockInputLogs.Add(
new StockInputLog()
{
UserId = Session.Instance.User!.Id,
Amount = inputAmount,
Stock = new Stock()
{
ExpirationDate = expirationDate,
ItemId = Item!.Id,
// Removed: Amount = inputAmount,
}
}
);
...
Whenever I do insert an entity with its related entity, the trigger is executed twice. I don't think there's a problem in my code though.
CreateStockInputWindow.xaml.cs
StockInputLog.cs
Stock.cs
The current workaround is to insert an entity, then call dbContext.SaveChanges(), and finally insert the related entity by selecting the previous entity's id and do dbContext.SaveChanges()
Example.cs
But it looks ugly. Can anyone help to fix this problem? Thank you in advance