Giter Site home page Giter Site logo

koenbeuk / entityframeworkcore.triggered Goto Github PK

View Code? Open in Web Editor NEW
487.0 487.0 26.0 1.58 MB

Triggers for EFCore. Respond to changes in your DbContext before and after they are committed to the database.

License: MIT License

C# 100.00%
dotnet efcore entity-framework entity-framework-core sql trigger

entityframeworkcore.triggered's People

Contributors

benmccallum avatar cdimitroulas avatar koenbeuk avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

entityframeworkcore.triggered's Issues

How can we listen to the server trigger from a JS client app?

Hi;
In Firebase Firestore system, Google offers a client SDK to connect to backend Firesore. Part of this SDK, is a listener method that we can subscribe to and anytime there is a change in the DB, this listener fetch the changed data and provides it to us at client side. And if we have our data bound to the view, the view shows new data.

Using your product that gets triggered on the server side, what would be the best strategy to accomplish the same on the client side, by having a listener?

Thank you for great work.

The trigger executed twice

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

...
                    dbContext.StockInputLogs.Add(
                        new StockInputLog()
                        {
                            UserId = Session.Instance.User!.Id,
                            Amount = inputAmount,
                            Stock = new Stock()
                            {
                                ExpirationDate = expirationDate,
                                ItemId = Item!.Id,
                                Amount = inputAmount,
                            }
                        }
                    );
...

StockInputLog.cs

namespace WarehouseApp.Models;

public class StockInputLog : BaseModel
{
    public int StockId { get; set; }

    public int UserId { get; set; }

    public int Amount { get; set; }

    public Stock? Stock { get; set; }

    public User? User { get; set; }
}

Stock.cs

using System;
using System.Collections.Generic;

namespace WarehouseApp.Models;

public class Stock : BaseModel
{
    public DateTime ExpirationDate { get; set; }

    public int ItemId { get; set; }

    public int Amount { get; set; }

    public Item? Item { get; set; }

    public List<StockInputLog>? StockInputLogs { get; set; }

    public List<StockOutputLog>? StockOutputLogs { get; set; }
}

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

        dbContext.AddRange(
            new User(username: "admin", name: "Admin", password: "Password", isAdmin: true),
            new User(username: "user", name: "User", password: "Password", isAdmin: false)
        );

        dbContext.AddRange(
            new Item(barcode: "8991038775416", name: "Lorem ipsum ", quantifier: "dus"),
            new Item(
                barcode: "8991038775417",
                name: "Dolor sit amet",
                quantifier: "kg",
                totalAmount: 0
            ),
            new Item(
                barcode: "8991038775418",
                name: "Consectetur adipiscing",
                quantifier: "m",
                totalAmount: 0
            )
        );

        dbContext.SaveChanges();

        dbContext.Add(new Stock() { ItemId = 1, ExpirationDate = DateTime.Now.AddDays(10) });

        dbContext.SaveChanges();

        dbContext.AddRange(
            new StockInputLog() { Amount = 10, UserId = 1, StockId = 1 },
            new StockInputLog() { Amount = 20, UserId = 1, StockId = 1 },
            new StockInputLog() { Amount = 15, UserId = 1, StockId = 1 }
        );

        dbContext.SaveChanges();

But it looks ugly. Can anyone help to fix this problem? Thank you in advance

Scoped Dependency Issue

This issue may prove tricky to repro, so I'll explain the issue and you can advise on further action.

This image shows some logs containing the HttpContext TraceIdentifier, the DbContext Id and a Guid in a scoped dependency RequestAuthVariables.

image

The highlighted rows show that two different HttpContexts get the same instance of RequestAuthVariables.

This is an intermittent issue in our production system and we've been able to reproduce in an integration test using a Parallel.ForEachAsync loop of 100. A loop of 10 typically works correctly and even the loop of 100 occasionally works.

Our interim solution is to register our triggers as Transient instead of Scoped.

We're using the AddTriggeredDbContextPool and AddTriggeredPooledDbContextFactory

@benmccallum and I are happy to try and work with you as needed.

Cannot trigger all AfterSaveCompleted() if 2 triggers all implement IAfterSaveCompletedTrigger

ATrigger as below.

public ATrigger : IAfterSaveTrigger<A>, IAfterSaveCompletedTrigger {
    private List<string> _aNames = new List<string>();
    public Task AfterSave(ITriggerContext<A> context, CancellationToken cancellationToken) { 
        if (context.ChangeType == ChangeType.Added) { 
            this._aNames.Add(context.Name);
            return Task.CompletedTask;
        }
    }

        public Task AfterSaveCompleted(CancellationToken cancellationToken) {
            Console.WriteLine($"There are {_aNames.Count()} As to {_aNames.Distinct().Count()}" distinct A names");
            return Task.CompletedTask;
        }
}

BTrigger as below.

public BTrigger : IAfterSaveTrigger<B>, IAfterSaveCompletedTrigger {
        private List<string> _bNames = new List<string>();
        public Task AfterSave(ITriggerContext<B> context, CancellationToken cancellationToken) { 
            if (context.ChangeType == ChangeType.Added) { 
                this._bNames.Add(context.Name);
                return Task.CompletedTask;
            }
        }

        public Task AfterSaveCompleted(CancellationToken cancellationToken) {
            Console.WriteLine($"There are {_bNames.Count()} Bs to {_bNames.Distinct().Count()}" distinct B names");
            return Task.CompletedTask;
        }
}

AfterSave in both ATrigger and BTrigger could be triggered by SaveChanges(), but only ATrigger's AfterSaveCompleted triggered in the end.

The entity type 'Dictionary`2Proxy' was not found

I am using this library to trigger notifications on updates to my entities.
For that I created a trigger where I call my MailService to send mails via MailJet.

In that MailService I keep track of the mails send and save that as records in a separate table, so I can check later wich mails have been send already.

This worked fine without the triggers but now that I copied the notification code into a trigger I get the following error:
The entity type 'Dictionary`2Proxy' was not found. Ensure that the entity type has been added to the model.

When I comment the line where the MailService saves the send mails to the database it works, so based on the stack trace below I would assume that this library somehow captures changes that should not be tracked.

info: EntityFrameworkCore.Triggered.TriggerSession[0]
      Invoking trigger: iTS_Service.Triggers.Notifications.UpdateAppointmentTrigger as EntityFrameworkCore.Triggered.IAfterSaveAsyncTrigger`1[iTS_Service.Data.Models.Appointment]
fail: Microsoft.EntityFrameworkCore.Update[10000]
      An exception occurred in the database while saving changes for context type 'iTS_Service.Data.ApplicationDbContext'.
      System.InvalidOperationException: The entity type 'Dictionary`2Proxy' was not found. Ensure that the entity type has been added to the model.
         at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.GetOrCreateEntry(Object entity)
         at Microsoft.EntityFrameworkCore.DbContext.EntryWithoutDetectChanges(Object entity)
         at Microsoft.EntityFrameworkCore.DbContext.Entry(Object entity)
         at EntityFrameworkCore.Triggered.Internal.TriggerContextTracker.CaptureChanges()
         at EntityFrameworkCore.Triggered.TriggerSession.CaptureDiscoveredChanges()
         at EntityFrameworkCore.Triggered.Internal.TriggerSessionSaveChangesInterceptor.SavingChangesAsync(DbContextEventData eventData, InterceptionResult`1 result, CancellationToken cancellationToken)
         at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)

Also I checked, there are no dictionaries in any of my models for the database as thats not supported by EF Core. There are once in local variables but commenting those out does not fix the problem and it also should not be tracked regardless.

My code:
My "UpdateAppointmentTrigger"

Can only set applicationScopedServiceProvider once

Version
3.1.0

Since 3.1.0, our web applications that use Autofac have been experiencing this error:
Can only set applicationScopedServiceProvider once

I can try and create a repro solution if needed, but just wanted to check first to see whether there is anything obvious I can do to resolve the issue? When debugging into the symbols, both the _applicationScopedServiceProvider and the serviceProvider passed in are instances of AutofacServiceProvider but they must be different because it's passing the != check.

Version 3.0.0 does not have this issue. Our micro-services projects (that don't use Autofac) don't have this issue.

Notwork with DbContextFactory<DbContext>

Do not work with DbContextFactory<DbContext>.
With a certain version of the framework, the "AddDbContextFactory" method has become available, which makes it possible to pass a factory to the constructor.

            services.AddDbContextFactory<DataContext>(optionsBuilder =>
            {
                optionsBuilder.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"));
                optionsBuilder.UseTriggers();
            });

If we transfer a factory, or a pool of factories, then your triggered solution does not work.

Transient triggers should share state between its life-cycle implementations

Currently transient triggers do not share state between the different lifecycles, e.g;

class MyTrigger : IBeforeSaveTrigger<object>, IAfterSaveTrigger<object> { 
    int _counter;
    
    public Task BeforeSave(ITriggerContext<object> context, CancellationToken _) {
        _counter += 1;
        return Task.CompletedTask;
    }
    
    public Task AfterSave(ITriggerContext<object> context, CancellationToken _) {
        Console.WriteLine($"Counter: {_counter}");
        return Task.CompletedTask;
    }
}

Will always print 0 when registered as a transient trigger since state is not shared between the 2 lifecycles. If we were to register the trigger as a Scoped trigger then the counter may be > 1 if there are multiple entities within the changeset.

It makes sense to have transient triggers still share the same state so that a Trigger is only transient per unique Entity/Trigger, not per unique Entity/Lifecyle as its currently.

Risk using async trigger behind a sync SaveChanges() call?

Hey! We've still got plenty of sync SaveChanges() calls in our codebase that we're slowly getting rid of but it did make me wonder whether there's a risk to writing an async trigger behind a sync call.

Before we dive in fully we might need to eliminate all our legacy sync SaveChanges() calls.

Multiple triggers issue with disposed DBContext

I run into an issue that the DBContext is disposed after the first trigger is done and when this trigger changes another entity with a trigger in it as well, then the second trigger fails, because the db context is already disposed.

I set the DBContext to transient, but still the same issue. Is there some more info on states of this NuGet and how to solve this?

Subtle issues when validating entities

I've been thinking about the subtleties of validating entities that depend on values in relations. For example, a parent entity representing a group of Foo might have a FooType property, and its child entities must all have the same FooType. The triggers for both the parent and child entities would examine related entities to validate a new or changed entity's FooType.

But we also have to consider that related entities may have uncommitted changes. The problem can be minimized by keeping units of work small. Still, it's impossible to anticipate what combinations of changes may be saved together.

So the principle I've come up with is that a trigger should always assume that unsaved changes to other entities are valid, and base the validation of its own entity on the modified values of the other entities. If this assumption is incorrect, the triggers responsible for the other entities should abort the transaction.

Triggers may also need to account for the intricacies of DetectChanges.

Are there any other concerns along these lines that I should be aware of? I didn't see anything in the wiki about this kind of thing.

Autofac integration

Hello.

Saw your library, cool idea!
But, unfortunately, in my project the triggers are not fired.

All your examples use microsoft di, but my project uses autofac

I looked at this link, but it didn't work

How can I solve my problem?

my registration code:

builder.Register(c =>
            {
                var store = c.Resolve<ITenantStore>();
                var accessor = c.Resolve<IHttpContextAccessor>();
                Tenant tenant = null;
                if (accessor.HttpContext != null)
                {
                    tenant = accessor.HttpContext.GetTenant();
                    var options = new DbContextOptionsBuilder<CRMContext>()
                        .UseNpgsql(tenant.ConnectionString, x => x.CommandTimeout(60))
                        .Options;

                    var context = new CRMContext(options)
                    {
                        Tenant = tenant.HostIdentifier
                    };

                    return context;
                }

my dbcontext configuration:

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
            optionsBuilder
                .UseSnakeCaseNamingConvention()
                .UseTriggers(triggerOptions => triggerOptions.AddAssemblyTriggers());
        }

and my trigger:

public class ItemTrigger : IBeforeSaveTrigger<Item>
    {
        private readonly DataConnection _linqToDb;

        public ItemTrigger(DataConnection linqToDb)
        {
            _linqToDb = linqToDb;
        }

        public Task BeforeSave(ITriggerContext<Item> context, CancellationToken cancellationToken)
        {
            if (context.Entity.ParentId == null)
            {
                context.Entity.FullName = context.Entity.Name;
            }
            else
            {
                context.Entity.FullName = GetFullName(context.Entity.ParentId.Value) + " -> " + context.Entity.Name;
            }

            return Task.CompletedTask;
        }

        private async Task<string> GetFullName(int id, string separator = " โ†’ ")
        {
            var cte = _linqToDb.GetCte<Item>(x =>
            {
                return _linqToDb.GetTable<Item>()
                    .Where(item => item.Id == id)
                    .Select(item => new Item { Id = item.Id, ParentId = item.ParentId, Index = 1, Name = item.Name })
                    .Concat
                    (
                        from i in _linqToDb.GetTable<Item>()
                        join c in x on i.Id equals c.ParentId
                        select new Item
                        {
                            Id = i.Id,
                            ParentId = i.ParentId,
                            Index = c.Index + 1,
                            Name = i.Name,
                        }
                    );
            });

            var name = string.Empty;
            var items = await cte.OrderByDescending(x => x.Index).ToListAsync();

            foreach (var item in items)
            {
                name += item.Name + (item.Id == id ? "" : separator);
            }

            return name;
        }
    }
``

EF core version: 5.0.10
EntityFrameworkCore.Triggered version: 2.3.2

Please, help me!

Support for .NET 6

Hi Koen,

hope you are doing well! I just tried out to upgrade to .NET 6 RC1 and noticed that unfortunately some exceptions are thrown. Do you plan to support .NET 6 soon or will you wait for the GA release?

Error:

An error occurred while starting the application.
TypeLoadException: Method 'GetServiceProviderHashCode' in type 'ExtensionInfo' from assembly 'EntityFrameworkCore.Triggered, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e4acff2b88bee728' does not have an implementation.

EntityFrameworkCore.Triggered.Infrastructure.Internal.TriggersOptionExtension.get_Info() in TriggersOptionExtension.cs, line 55

	TypeLoadException: Method 'GetServiceProviderHashCode' in type 'ExtensionInfo' from assembly 'EntityFrameworkCore.Triggered, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e4acff2b88bee728' does not have an implementation.
		EntityFrameworkCore.Triggered.Infrastructure.Internal.TriggersOptionExtension.get_Info() in TriggersOptionExtension.cs

		public DbContextOptionsExtensionInfo Info => (DbContextOptionsExtensionInfo) this._info ?? (DbContextOptionsExtensionInfo) (this._info = new TriggersOptionExtension.ExtensionInfo((IDbContextOptionsExtension) this));

Microsoft.EntityFrameworkCore.DbContextOptions.GetHashCode()
System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue>.GetOrAdd<TArg>(TKey key, Func<TKey, TArg, TValue> valueFactory, TArg factoryArgument)
Microsoft.EntityFrameworkCore.Internal.ServiceProviderCache.GetOrAdd(IDbContextOptions options, bool providerRequired)
Microsoft.EntityFrameworkCore.DbContext..ctor(DbContextOptions options)
Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken>..ctor(DbContextOptions options)
Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken>..ctor(DbContextOptions options)
Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityDbContext<TUser, TRole, TKey>..ctor(DbContextOptions options)
LC.Box.Service.OrderManagement.Infrastructure.Contexts.AuditableDbContext..ctor(DbContextOptions options, IClientConfiguration clientConfiguration, ICurrentUserService currentUserService, ILogger<LabCubeOrderDbContext> logger) in AuditableDbContext.cs

Thanks in advance! :)

Trigger not firing when using ExecuteDeleteAsync in EF Core 7.0

Hi all,

I'm having an issue with the new feature in EF Core 7.0 when using ExecuteDeleteAsync and ExecuteUpdateAsync. Specifically, the trigger is not firing because the ExecuteDeleteAsync method works as one SQL command.

Has anyone else encountered this issue, and if so, how did you solve it? Any insights or suggestions would be greatly appreciated.

Thanks in advance.

UseTriggers in startUp

Hi
i have million rows in Cities, States, Countries. However when i user trigger in start this slows how my initial seeding setup. I have waited like 10 minutes and then i have to comment below lines and then uncomment after initial seeding. I'm not sure whats happening under the hood that makes seeding so slow. I am not even triggering any event during Cities, States and Countries seeding.

// .UseTriggers(triggerOptions =>
            // {
            //     triggerOptions.AddTrigger<DeleteCustomerDocumentTrigger>();
            //     triggerOptions.AddTrigger<DeletePhysicialFileTrigger>();
            //     triggerOptions.AddTrigger<DeleteProviderDocumentTrigger>();
            // })

[Owned] entities does not trigger hooks

A is an owned entity. There are 2 other entities B and C which both use A. E.g.

[Owned]
public class A {
  // ...
}

public class B {
  public A MyA { get; set; }
  // ...
}

public class C {
  public A MyA { get; set; }
  // ...
}

Current behavior: You can't add triggers to A (reasonable). Whenever any change on A happens, no triggeres are triggered (e.g. on B or C).

Expected behavior: Whenever anything changes on B.MyA, any trigger for B is triggered. Whenever anything changes on C.MyA, any trigger for C is triggered.

Getting a service scoped to a dbcontext from a trigger

Hey!

I've got a use case where I'd like to generically track a bunch of changes to certain entities after a save, group them by a key, then fire notifications/emails off for those keys.

I gave this a go via the following setup:

  • Scoped service SupplierDataChangedContext which has a list property that changes can be added to.
  • An after save trigger IAfterSaveTrigger<ISupplierData> which injects SupplierDataChangedContext and adds to its list relevant changes.
  • IAfterSaveCompletedTrigger called SupplierDataChangedPublisher which injects the SupplierDataChangedContext and does the aggregation of supplier data changes and sending of notifications

The problem I feel like I have though is that ideally SupplierDataChangedContext would be scoped to the DbContext the trigger has.

So I guess I'm wondering:

  1. If I inject my DbContext into a trigger, is that guaranteed to be the same one that the trigger fired from?
  2. How can I get a hold of a service who's lifetime is the same as the DbContext, or in the case of pooling, the rented lifetime?

Thanks, Ben

Detect exactly which column is changed

Hello

I want to create an audit for those properties whose old and new values are different. Is there a way to detect which property is changed on the AfterSave method?

Waiting for your answer!

Best Wishes
Saad

Trigger BeforeSave executes, however new data is not saved in database fields.

hi

Following Getting Started, everything appears to be correct in my implementation, including the fact that triggers are executed.

However, new data is not saved in the database.

Details

Trigger

public class EmpresasSetChangedAtTrigger : IBeforeSaveTrigger<Empresa>
    {

        public Task BeforeSave(ITriggerContext<Empresa> context, CancellationToken cancellationToken)
        {

            if (context.ChangeType == ChangeType.Modified)
            {
                context.Entity.ChangedAt = DateTimeOffset.Now;
            }

            return Task.CompletedTask;
        }

    }

Register DbContext in Startup

services.AddDbContext<TenantDbContext>((serviceProvider, optionsBuilder) =>
            {
                var CurrentTenant = serviceProvider.GetRequiredService<ICurrentTenant>();

                var TenantConnString = MultiTenantUtils.TenantConnStringFactory(CurrentTenant, _configuration);

                optionsBuilder.UseSqlServer(TenantConnString);

                optionsBuilder.UseTriggers(triggerOptions =>
                {
                    var TriggerAssembly = CurrentAssemblyUtils.GetCurrent();
                    triggerOptions.AddAssemblyTriggers(TriggerAssembly);
                });

            })
            //.AddScoped<IBeforeSaveTrigger<Nfe>, NfeSetChangedAtTrigger>()
            //.AddTransient<IBeforeSaveTrigger<Empresa>, EmpresasSetChangedAtTrigger>()
            ;

Any Tips ?

Triggers not firing at all

I'm using 3.2.2 with Microsoft.EntityFrameworkCore.SqlServer 7.0.5 in a project targeting net6.0. My test trigger is not being called when I save changes to a User entity:

public class TestTrigger : IBeforeSaveTrigger<User>
{
	public TestTrigger()
	{
		; // breakpoint never hit
	}

	public Task BeforeSave(ITriggerContext<User> context, CancellationToken cancellationToken)
	{
		return Task.CompletedTask; // breakpoint never hit
	}
}

I think I've tried every way of registering this trigger. I'd prefer this way, since all triggers will be in the same assembly:

services.AddDbContext<AppContext>(options => options
		.UseSqlServer(builder.Configuration["ConnectionString"])
		.UseTriggers(options => options.AddAssemblyTriggers(ServiceLifetime.Scoped, typeof(Program).Assembly))
	);

Am I overlooking something incredibly obvious?

Rebrand Recursion as Cascading

Currently I'm using Recursion to refer to the feature that a BeforeSaveTrigger can add/modify/delete its current Entity, or other entities within the same DbContext, when then will trigger subsequent triggers that are interested in those changes.

In database land this feature is really called Cascading. The term recursion came from an old project. As cascading is likely already used in your database and certainly in EF, and since it better represents the actual feature, I'm considering refactoring a rename.

This can be done by first introducing Cascading aliases and in a future version, deprecating the 'Recursive' aliases.

Database Rollback

how can i rollback automaticly all changes when trigger is failed? Can i use transaction?

DbContextFactory created with Scoped lifetime instead of Singleton

I have seen previous issues about DbContextFactory and the suggestion was to use AddTriggeredDbContextFactory to solve those problems.

However, when we tried to do this it caused a bunch of other issues throughout the codebase because AddTriggeredDbContextFactory seems to cause the context factory to be changed to a scoped lifetime somehow.

We found that using services.AddDbContextFactory<TDbContext>(builder => { ...}, ServiceLifetime.Transient); solved the issues for us.

I am interested to know if you see any potential issues with using the context factory with a transient service lifetime like this? Our automated tests and passing and some manual tests show that the triggers are working correctly as expected but I worry that there might be some unexpected consequences that will bite us later down the line.

Ilogger in IAfterSaveTrigger<T> class causes DI error

Following the wiki examples, I created my trigger class as

` public class DeviceMessageTrigger : IAfterSaveTrigger
{
protected readonly JAoTSQLContext _context;
protected readonly ILogger _logger;

    public DeviceMessageTrigger(JAoTSQLContext context, ILogger<DeviceMessageTrigger> logger)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task AfterSave(ITriggerContext<Device_Message> context, CancellationToken cancellationToken)
    {
        switch (context.ChangeType)
        {
            case ChangeType.Added:
                _logger.LogDebug("Executing Trigger Action");
                await _context.ServiceBroker_DeviceMessageAdded(context.Entity.DeviceMessageID, context.Entity.DeviceID);
                break;

------ continue
`

and registered the trigger in both the context itself

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer(_connectionString, x => { x.UseNetTopologySuite(); x.CommandTimeout(GetConnectionTimeout()); }); optionsBuilder.UseTriggers(o => { o.AddTrigger<DeviceMessageTrigger>(); o.AddTrigger<ConfigurationDownlinkPayloadTrigger>(); }); } }
and in the Progam.Main services configuration:

services.AddDbContext<JAoTSQLContext >(options => { options.UseSqlServer(); options.UseTriggers(t => { t.AddTrigger<DeviceMessageTrigger>(); t.AddTrigger<ConfigurationDownlinkPayloadTrigger>(); }); });

Compilation and application startup are OK, but as soon as an insert into the table is run I get this error stating that the DI cannot create the logger instance.

ERROR|DataConnectors.SQLConnector.Log_DefaultError|Error raised while executing operation SaveDeviceMessageData.|System.InvalidOperationException: Unable to resolve service for type 'Microsoft.Extensions.Logging.ILogger1[DataConnectors.EFCore.EFTrigger.DeviceMessageTrigger]' while attempting to activate 'DataConnectors.EFCore.EFTrigger.DeviceMessageTrigger'. at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired) at lambda_method878(Closure, IServiceProvider, Object[]) at EntityFrameworkCore.Triggered.Internal.TriggerInstanceFactory1.Create(IServiceProvider serviceProvider)
at EntityFrameworkCore.Triggered.Internal.TriggerFactory.Resolve(IServiceProvider serviceProvider, Type triggerType)+MoveNext()
at EntityFrameworkCore.Triggered.Internal.TriggerDiscoveryService.DiscoverTriggers(Type openTriggerType, Type entityType, Func2 triggerTypeDescriptorFactory) at EntityFrameworkCore.Triggered.TriggerSession.RaiseTriggers(Type openTriggerType, Exception exception, ITriggerContextDiscoveryStrategy triggerContextDiscoveryStrategy, Func2 triggerTypeDescriptorFactory, CancellationToken cancellationToken)
at EntityFrameworkCore.Triggered.Internal.TriggerSessionSaveChangesInterceptor.SavedChangesAsync(SaveChangesCompletedEventData eventData, Int32 result, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
at DataConnectors.SQLConnector.<>c__DisplayClass138_0.<g__SaveDeviceMessage|0>d.MoveNext() in C:\Develop\Projects\Repos\JAoT.Core\DataConnectors\SQLConnector.cs:line 5469
--- End of stack trace from previous location ---
at DataConnectors.SQLConnector.<>c__DisplayClass138_0.<b__1>d.MoveNext() in C:\Develop\Projects\Repos\JAoT.Core\DataConnectors\SQLConnector.cs:line 5435
--- End of stack trace from previous location ---
at DataConnectors.SQLConnector.DataContextExecuteAsync[T](Func2 action, IServiceProvider serviceProvider) in C:\Develop\Projects\Repos\JAoT.Core\DataConnectors\SQLConnector.cs:line 114 at DataConnectors.SQLConnector.DataContextExecuteAsync[T](Func2 action) in C:\Develop\Projects\Repos\JAoT.Core\DataConnectors\SQLConnector.cs:line 103
at DataConnectors.SQLConnector.SaveDeviceMessageData(IDeviceMessage deviceMessage) in C:\Develop\Projects\Repos\JAoT.Core\DataConnectors\SQLConnector.cs:line 5433

Please note that if I remove the logger (leaving the data context) the operations are fully successful: it looks like the problem is only affecting the logger, not the data context.

I also tried to get the ILoggerFactory object from the DI:

` public class DeviceMessageTrigger : IAfterSaveTrigger<Device_Message>
{
protected readonly JAoTSQL _context;
protected readonly ILogger _logger;

    public DeviceMessageTrigger(JAoTSQL context, ILoggerFactory loggerfactory)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
        _logger = loggerfactory?.CreateLogger<DeviceMessageTrigger>() ?? throw new ArgumentNullException(nameof(loggerfactory));
    }

    public async Task AfterSave(ITriggerContext<Device_Message> context, CancellationToken cancellationToken)
    {
        switch (context.ChangeType)
        {
            case ChangeType.Added:
                _logger.LogError("Executing Trigger Action");
                await _context.ServiceBroker_DeviceMessageAdded(context.Entity.DeviceMessageID, context.Entity.DeviceID);
                break;

`

This way I do not get any error, and the trigger is called and executed as expected. But the logger does not write anything even if I made sure the instance was created successfully by the ILoggerFactory and I raised the Log level to Error to avoid the log entry to be filtered.
I checked and can confirm the async trigger is called only after SaveAsync calls not to mix sync and async code and, in addition, please note that I moved the logging configuration before and after the configuration of the db context and trigger components, but did not change the behavior.

Did I miss anything or is it a problem with the Triggers?

Thank you

Could not load file or assembly 'EntityFrameworkCore.Triggered.Abstractions'

Hello Mr. Koen.

Thank you for your excellent contributions in GitHub. I have installed EntityFrameworkCore.Triggered version 1.4.0. Everything works perfectly in Windows applications. When I tried to use it in an ASP.NET Website project I got the following error (note Abstractions):

Could not load file or assembly 'EntityFrameworkCore.Triggered.Abstractions' or one of its dependencies. Strong name signature could not be verified. The assembly may have been tampered with, or it was delay signed but not fully signed with the correct private key. (Exception from HRESULT: 0x80131045)

I have tried almost every possible workaround I found in google but nothing works.

I can't update to newer .NET versions (3.1+) due to client financial restrictions.

Would you kindly give me a hint at any possible workaround? Thank you in advance.

  • Jasem

Disable triggers temporarly

Is there a possibility to disable/enable the triggers for a specific part of the code? i.e. during the seeding of the database?

[Performance] Sync. / ValueTask version of IBeforeSaveTrigger<T>

Given the Trigger below:

Notice that we're not doing anythingasync, aren't we always setting up a Task state machine for nothing?

public class EntityBeforeSaveTrigger : IBeforeSaveTrigger<Entity>
{
    public Task BeforeSave(ITriggerContext<Entity> context, CancellationToken cancellationToken)
    {
        if (context.ChangeType == ChangeType.Added)
        {
            context.Entity.CreatedAt = DateTime.UtcNow;
            context.Entity.UpdatedAt = DateTime.UtcNow;
        }

        if (context.ChangeType == ChangeType.Modified)
        {
            context.Entity.UpdatedAt = DateTime.UtcNow;
        }
        return Task.CompletedTask; // Should not be needed.
    }
}

We can't implement the void overload although it's part of the package:
IBeforeSaveTrigger

Notice that the compiler will complain about Interface not implemented

public class EntityBeforeSaveTrigger : IBeforeSaveTrigger<Entity>
{
    public void BeforeSave(ITriggerContext<Entity> context)
    {
        if (context.ChangeType == ChangeType.Added)
        {
            context.Entity.CreatedAt = DateTime.UtcNow;
            context.Entity.UpdatedAt = DateTime.UtcNow;
        }

        if (context.ChangeType == ChangeType.Modified)
        {
            context.Entity.UpdatedAt = DateTime.UtcNow;
        }
    }
}

Why is this and can't we create a ValueTask overload?

Version: 3.2.1

PS: Package is awesome.

Trigger Event On Sub Entity Save

Apologies if I'm being slow with this or I'm failing to find the documentation for it.

I have existing system, large and complex. I have used this before for smaller things but here I'm trying to remove existing records from a repo when a new record is added. As they can be added in unknown numbers of places, I'm using the library to trigger a clear before save on this Entity.

BUT. It seems a number of records are being added as a sub entity of another object. These end up in the DB without triggering the .. trigger.

Is there a way to configure the trigger to also fire if it is a sub entity of another record? Or can it not detect this. Or worse, should it already be working and for me its not.

Thanks,

In unittesting

I have a trigger class called MyTriggerClass that takes a constructor with params DbContext and ILogger. This works great in my application. But now I want to run unit test against the DBContext and I registered the trigger and the trigger should fire. But when I try to run the unit tests I get an error saying:
Unable to resolve service for type ILogger while attempting to activate MyTriggerClass

Is there a way to initialize my own instance of the trigger class on UseTriggers? Or do you know how I should dependency inject my instance of Logger to MyTriggerClass in unit tests?

Re-open #154

See my comment in #154 - can close this issue after #154 is re-opened (assuming you agree ofc)

How to Use context.Items?

    /// <summary>
    /// Gets or sets a key/value collection that can be used to share data within the scope of this Entity
    /// </summary>
    IDictionary<object, object> Items { get; }

How to Use  context.Items?

In some cases, we need certain values in the DTO, but these values do not exist in the entity๏ผŒ
Is there a way this trigger can be handled?

Abstract classes are trying to be instantiated with dependency injection

I am trying to implement an abstract class seen below

`

    public abstract class BaseAfterSaveTrigger<TEntity> : IAfterSaveTrigger<TEntity>
    where TEntity : class
{

    private readonly OriginalTechSystemsContext dbcontext;

    public BaseAfterSaveTrigger(OriginalTechSystemsContext dbcontext)
    {
        this.dbcontext = dbcontext;
    }

    public async Task AfterSave(ITriggerContext<TEntity> context, CancellationToken cancellationToken)
    {
        var entity = context.Entity;

        var task = context.ChangeType switch
        {
            ChangeType.Added => OnAdded(entity),
            ChangeType.Modified => OnAdded(entity),
            ChangeType.Deleted => OnAdded(entity),
            _ => throw new System.NotImplementedException(),
        };

        await task;
        await dbcontext.SaveChangesAsync(cancellationToken);
    }

    public virtual Task OnAdded(TEntity addedEntity) => Task.CompletedTask;

    public virtual Task OnModified(TEntity modifiedEntity) => Task.CompletedTask;

    public virtual Task OnDeleted(TEntity deletedEntity) => Task.CompletedTask;
}

`

I am getting the following error
image

I believe the DI system is not ignoring abstract classes

Triggers not firing intermittently

Version
3.2.1
Running in net7.0 aspnet application

Scenario

Some code omitted for brevity.

    public class SubmitFleetBookingForReviewHandler
        : IRequestHandler<SubmitFleetBookingForReviewInput, SubmitFleetBookingForReviewOutput>
    {
        private readonly ILogger<SubmitFleetBookingForReviewHandler> _logger;
        private readonly IRequestAuthVariables _requestAuthVars;
        private readonly ISupplierAuthorizer _supplierAuthorizer;
        private readonly IClock _clock;
        private readonly AutoGuruDbContext _db;
        private readonly IBookingStatusTransitionValidator _bookingStatusTransitioner;
        private readonly IRequestHandler<AcceptFleetSignOnAgreementRequest, Unit> _acceptFleetSignOnAgreementHandler;
        private readonly IRequestHandler<AssessBookingBusinessRulesRequest, BookingBusinessRuleBookingAssessment> _assessBookingBusinessRulesHandler;

        public SubmitFleetBookingForReviewHandler(
            AutoGuruDbContext db,
        )
        {
            _db = db;
        }

        public async Task<SubmitFleetBookingForReviewOutput> Handle(
            SubmitFleetBookingForReviewInput request,
            CancellationToken cancellationToken)
        {
            <code here that changes the booking>

            try
            {
                _logger.LogDebug("Before 1st save. Booking Id = {bookingId} Booking Status Id = {bookingStatusId} DbContextId = {dbContextId} Lease = {lease}",
                    bookingId, booking.BookingStatusID, _db.ContextId.InstanceId, _db.ContextId.Lease);
                _logger.LogDebug("Changes before 1st save. {changes}", _db.ChangeTracker.DebugView.LongView);

                await _db.SaveChangesAsync(CancellationToken.None);

                _logger.LogDebug("After 1st save. Booking Id = {bookingId} Booking Status Id = {bookingStatusId} DbContextId = {dbContextId} Lease = {lease}",
                    bookingId, booking.BookingStatusID, _db.ContextId.InstanceId, _db.ContextId.Lease);
                _logger.LogDebug("Changes after 1st save. {changes}", _db.ChangeTracker.DebugView.LongView);

                _logger.LogInformation("Fleet booking ({bookingId}) submitted for review,", bookingId);
            }
            catch (DbUpdateConcurrencyException)
            {
                return new SubmitFleetBookingForReviewOutput(bookingId, new ConcurrentUpdateError());
            }

            <code here that changes the booking again>

            _logger.LogDebug("Before 2nd save. Booking Id = {bookingId} Booking Status Id = {bookingStatusId} DbContextId = {dbContextId} Lease = {lease}",
                bookingId, booking.BookingStatusID, _db.ContextId.InstanceId, _db.ContextId.Lease);
            _logger.LogDebug("Changes before 2nd save. {changes}", _db.ChangeTracker.DebugView.LongView);

            await _db.SaveChangesAsync(CancellationToken.None);

            _logger.LogDebug("After 2nd save. Booking Id = {bookingId} Booking Status Id = {bookingStatusId} DbContextId = {dbContextId} Lease = {lease}",
                bookingId, booking.BookingStatusID, _db.ContextId.InstanceId, _db.ContextId.Lease);
            _logger.LogDebug("Changes after 2nd save. {changes}", _db.ChangeTracker.DebugView.LongView);
        }
    }

We have a number of triggers attached to the Booking, some IBeforeSaveTrigger and some IAfterSaveTrigger.

Intermittently the triggers on Booking don't fire during the 2nd SaveChangesAsync() in the code above.

Logs

FailedLogs.txt

SuccessLogs.txt

Differences
When successful, 33 triggers are discovered. When it fails, the first 22 triggers match, but 11 are missing.

Invoking trigger: "AutoGuru.DataAccess.Triggers.Booking_BeforeSave_SetSupplierPaidAt" as "EntityFrameworkCore.Triggered.IBeforeSaveTrigger`1[AutoGuru.Entities.Booking]"
Invoking trigger: "AutoGuru.DataAccess.Triggers.Booking_BeforeSave_SetAutoGuruPaidAt" as "EntityFrameworkCore.Triggered.IBeforeSaveTrigger`1[AutoGuru.Entities.Booking]"
Invoking trigger: "AutoGuru.DataAccess.Triggers.Booking_BeforeSave_SetRatesAndDiscountsForFleetBookings" as "EntityFrameworkCore.Triggered.IBeforeSaveTrigger`1[AutoGuru.Entities.Booking]"
Invoking trigger: "AutoGuru.DataAccess.Triggers.Booking_BeforeSave_SetPayerAndPaymentTerms" as "EntityFrameworkCore.Triggered.IBeforeSaveTrigger`1[AutoGuru.Entities.Booking]"
Booking_BeforeSave_SetMerchantServiceFeeForFleetBooking. Booking Id = 6005225 % = 0.0350 $ = 14.37
Booking Id = 6005225 Fleet Company Id = 1 Supplier Id = 830 Booking Status Id = 33 DbContextId = fba0254d-8bb0-4555-adff-a648aa665397 Lease 26
Invoking trigger: "AutoGuru.DataAccess.Triggers.Booking_BeforeSave_SetMerchantServiceFeeForFleetBooking" as "EntityFrameworkCore.Triggered.IBeforeSaveTrigger`1[AutoGuru.Entities.Booking]"
Invoking trigger: "AutoGuru.DataAccess.Triggers.Booking_BeforeSave_SetLastReviewedPropertiesOfBookingTicketItems" as "EntityFrameworkCore.Triggered.IBeforeSaveTrigger`1[AutoGuru.Entities.Booking]"
Invoking trigger: "AutoGuru.DataAccess.Triggers.Booking_BeforeSave_SetDataFee" as "EntityFrameworkCore.Triggered.IBeforeSaveTrigger`1[AutoGuru.Entities.Booking]"
Invoking trigger: "AutoGuru.DataAccess.Triggers.Booking_BeforeSave_ResetClaimOnAction" as "EntityFrameworkCore.Triggered.IBeforeSaveTrigger`1[AutoGuru.Entities.Booking]"
Invoking trigger: "AutoGuru.Shared.Bookings.Booking_ConsumeSupplierTimeSlot" as "EntityFrameworkCore.Triggered.IBeforeSaveTrigger`1[AutoGuru.Entities.Booking]"
Invoking trigger: "AutoGuru.DataAccess.Triggers.CreationTimestampingTrigger" as "EntityFrameworkCore.Triggered.IBeforeSaveTrigger`1[AutoGuru.Entities.Abstractions.ICreationTimestamped]"
Invoking trigger: "AutoGuru.DataAccess.Triggers.UpdateTimestampingTrigger" as "EntityFrameworkCore.Triggered.IBeforeSaveTrigger`1[AutoGuru.Entities.Abstractions.IUpdateTimestamped]"

The logs show the DbContextId of the DbContext injected into the code above and the DbContextId used in the trigger discovery process (TriggerContextTracker). The same context is used (as expected) but the changes don't seem to be detected.

We're using the default cascade behavior and limit.

In terms of testing, it is sometimes very intermittent in that I could use JMeter to submit 1000 bookings and all are successful. In other tests, multiple could fail within the first 100 requests.

I will be doing further investigations and will update as I make discoveries.

Transaction Exception on Save Changes

Hello. I recently published a large update to a project I manage. One of the things that was added with the update was to shift away from database-side triggers to application-side triggers with EFC.T. It seemed to work well during beta testing, but in production there are random exceptions being thrown which are causing headaches. Refreshing right after the exception works as expected.

InvalidOperationException: The specified transaction is not associated with the current connection. Only transactions associated with the current connection may be used.

I initially assumed it was with some triggers that have the DbContext injected into them, thinking it was a new instance being injected. I've since removed those triggers. It didn't help. I then removed the simpler triggers that change simple properties, such as modified at and by. Also didn't work. I have one trigger left that's a little more complex, but it looks like I'll have to remove it and disable EFC.T from my project.

I'm sad to do that, but I need to have stability in the live system, and the triggers and or triggered context is interfering with that. Would you be able to look into this for a future update? I'd much prefer to use the triggers, especially application-side since database-side is difficult to manage.

My project is on ASP.NET Core 6 with EntityFramework Core 6 running on Windows Server 2019 1809 (I believe) IIS. Will be updating to 7 soon-ish, if that matters to you.

Thanks!

AfterSave trigger receives Entity = UnmodifiedEntity?

Hi, I am trying the triggers and I'm noticing that the AfterSave trigger receives the updated/modified entity so I can't determine whether the fields I'm watching have changed.
Is this so or am I seeing this because another setting in my app, like LazyLoading?

There seems to sometimes be an issue with the DI system when using IServiceScopeFactory

I'm not exactly sure what the issue is but the DI system is throwing an error when I use the IServiceScopeFactory and make a call the the database that will end up running one the Trigger system. From the stacktrace provided is seems to me like it is an issue within the Trigger library

Image of the issue
image

The caller of the issue
image

How the IServiceScopeFactory is injected into the constructor
image

Stack Trace of the issue

   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ThrowHelper.ThrowObjectDisposedException()
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at EntityFrameworkCore.Triggered.Internal.HybridServiceProvider.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetServices(IServiceProvider provider, Type serviceType)
   at EntityFrameworkCore.Triggered.Internal.TriggerFactory.<Resolve>d__3.MoveNext()
   at System.Linq.Enumerable.<SelectIterator>d__174`2.MoveNext()
   at System.Collections.Generic.EnumerableHelpers.ToArray[T](IEnumerable`1 source, Int32& length)
   at System.Linq.Buffer`1..ctor(IEnumerable`1 source)
   at System.Linq.OrderedEnumerable`1.<GetEnumerator>d__17.MoveNext()
   at System.Linq.Enumerable.SelectIPartitionIterator`2.MoveNext()
   at System.Linq.Enumerable.<CastIterator>d__64`1.MoveNext()
   at EntityFrameworkCore.Triggered.TriggerSession.<RaiseAfterSaveFailedStartingTriggers>d__25.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult()
   at EntityFrameworkCore.Triggered.Internal.TriggerSessionSaveChangesInterceptor.<SaveChangesFailedAsync>d__9.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult()
   at Microsoft.EntityFrameworkCore.DbContext.<SaveChangesAsync>d__56.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at OTS.Data.Extensions.DbContextExtensions.<SaveAsNoTrackingAsync>d__1.MoveNext() in C:\Users\jb090\source\repos\OTS\OTS.Data\Extensions\DbContextExtensions.cs:line 64
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at OTS.DataAccess.Abstractions.BaseEntityService`1.<Update>d__8.MoveNext() in C:\Users\jb090\source\repos\OTS\OTS.DataAccess\Abstractions\BaseEntityService.cs:line 84

AddTrigger() and AddAssemblyTriggers() don't fully register a trigger type if another is already registered for the same interface implementation

Good day. I've come to report an issue with the library's trigger type registration APIs.

For the context of this issue, let the following be considered:

  • DbCtx - a DbContext type;
  • Entity - an entity type, whose persistance of entities is managed through a configured DbCtx instance;
  • EntityBeforeTrigger - a registered trigger type that implements IBeforeSaveTrigger<Entity>;
  • EntityAfterTrigger1 - a registered trigger type that implements IAfterSaveTrigger<Entity>;
  • EntityAfterTrigger2 - another registered trigger type that implements IAfterSaveTrigger<Entity>;
  • all of the aforementioned types, except Entity, are defined in the same assembly.

The library makes it possible to register the 3 listed trigger types as services, through calls to the AddTrigger() and AddAssemblyTriggers() extension methods. However, doing so would prevent the EntityAfterTrigger2 type's AfterSave() method from ever being called by the library, assuming EntityAfterTrigger2 is registered after EntityAfterTrigger1.
All overloads of the AddTrigger() and AddAssemblyTriggers() methods are affected by this problem.

The above happens because the library's trigger type registration APIs only register at most one type for each possible interface implementation, using calls to services.TryAdd(). This prevents multiple trigger types that match the same interface implementation from being discovered by the TriggerSession.RaiseTriggers() method, which itself handles their dispatchment.
The following code snippet would allow the library to properly discover all 3 of the aforementioned trigger types:

services.AddScoped<IBeforeSaveTrigger<Entity>, EntityBeforeTrigger>();
services.AddScoped<IAfterSaveTrigger<Entity>, EntityAfterTrigger1>();
services.AddScoped<IAfterSaveTrigger<Entity>, EntityAfterTrigger2>();

The following code excerpts highlight the location of calls to services.TryAdd() within the trigger type registration APIs.

In the ServiceCollectionExtensions.RegisterTriggerTypes() static method:

foreach (var customTrigger in customTriggers)
{
services.TryAdd(new ServiceDescriptor(customTrigger, sp => sp.GetRequiredService(triggerImplementationType), ServiceLifetime.Transient)); ;
}

In the non-chaining ServiceCollectionExtensions.AddAssemblyTriggers() method overload:
foreach (var triggerType in triggerTypes)
{
if (!registered)
{
services.Add(new ServiceDescriptor(assemblyType, assemblyType, lifetime));
registered = true;
}
services.TryAdd(new ServiceDescriptor(triggerType, sp => sp.GetRequiredService(assemblyType), ServiceLifetime.Transient));
}

Trigger not invoked when SaveChanges is called from exception handler

I have the following situation:

My application implements a rather long and complex import process. Since it takes too much time to wrap it into a transaction, I have to keep track of created objects and perform cleanup in case of an exception being raised.

In parallel, I have added a trigger to ensure that a specific (soft) constraint is respected (it's actually checking that the operation doesn't cause the number count of one specific entity type doesn't go over the lincesed limit). When that triggers determines that the operation needs to be aborted, it raises a specialized exception.

My problem is that it seems that while that trigger works fine elsewhere in the application, it fails to prevent the entity creating in this import process.

Here is the pseudo-code for the issue:

var entity = new Entity();
dbContext.Add(entity);
try
{
  dbContext.SaveChanges(); // <-- this will cause the trigger to raise an exception
} catch {
  // Do some necessary cleanup
  //...
  
  dbContext.SaveChanges(); // <-- At this stage, the entity is still in the "added" state in the change tracker. The trigger doesn't get called anymore and therefore the entity is added.
}

How can I enforce the trigger to be called regardless of wether it has been called already before ? Is there an alternate way to ensure the second call does not add the entity to the model again? I have tried changing te state to "untracked" but that didn't help.

Issue after upgrading to .NET 6.0

Hi,

After upgrading to .NET 6.0 I am getting this error when trying to run application or run dotnet ef tools.

Method 'GetServiceProviderHashCode' in type 'ExtensionInfo' from assembly 'EntityFrameworkCore.Triggered, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e4acff2b88bee728' does not have an implementation.

This was caused as a result of using UseTriggers which internally causes this issues.

Thanks,
Oleg

Many-to-Many Trigger

Hello,

thanks for the library. I'm wondering if it's possible to get a trigger working for a many-to-many relationship which is configured with:

modelBuilder.Entity<Author>()
    .HasMany(x => x.Books)
    .WithMany(x => x.Authors);

I pushed a small example where I tried to get it working, you can see that the Triggers for Author and Book are not called when I save a change to the collection.

Does not seem to work in .NET 6 minimal api

Hey,

We've been trying to use the trigger library in our project.

This is using a hosted service and a context + repository that lives in another project. There is a project reference to this. I did read upon the dependency chapter on the wiki, but I was not sure what was meant there with the service provider. Might not be totally aware of something, sorry if I'm not.

(Using version 3.0.0).

The following code does not throw an exception, but also does not trigger the trigger:

Program.cs:

using System.Net;
using EntityFrameworkCore.Triggered;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication
            .CreateBuilder(args);
        var env = builder.Environment.EnvironmentName;

        var config = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json", true, true)
            .AddJsonFile($"appsettings.{env}.json", true)
            .AddJsonFile("commonsettings.json", true, true)
            .AddJsonFile($"commonsettings.{env}.json", true)
            .AddEnvironmentVariables()
            .Build();

        Log.Logger = new LoggerConfiguration()
            .ReadFrom.Configuration(config)
            .CreateLogger();

        builder.Logging.AddSerilog();

        builder.Services
            .AddDbContext<BooksContext>(options =>
            {
                options.UseTriggers(triggerOptions =>
                {
                    triggerOptions.AddTrigger<BooksTrigger>();
                });
            })
            .AddScoped<IBookRepo, BookRepo>()
            .Configure<DatabaseConfiguration>(config.GetSection("Database"))
            .AddHostedService<BookMessageConsumer>(sp =>
            {
                using var scope = sp.CreateScope();
                var logger = scope.ServiceProvider.GetRequiredService<ILogger<BookMessageConsumer>>();

                return new BookMessageConsumer(sp, logger);
            });

        var app = builder.Build();

        app.MapGet("/alive", () => HttpStatusCode.OK);
        app.MapGet("/test", (BooksContext context) =>
        {
            context.Books.Add(new Book()
            {
                Title = 'Dumpert'
            });
            context.SaveChanges();
        });

        app.Run();
    }
}

Trigger code:

using System;
using System.Threading;
using System.Threading.Tasks;
using EntityFrameworkCore.Triggered;


public class BookTrigger : IAfterSaveTrigger<Book>, IBeforeSaveTrigger<Book>
{
    private readonly BookContext _bookContext;

    public BookTrigger(BookContext context)
    {
        _bookContext = context;
    }

    public Task AfterSave(ITriggerContext<Book> context, CancellationToken cancellationToken)
    {
        Console.WriteLine("trigger is working");
    }

    public Task BeforeSave(ITriggerContext<Book> context, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

On the subject of transactional triggers and detection of changes within a TriggerSession

Good day. I've come to report a few problems with the usability of transactional triggers provided by the library.

For the context of this issue, let the following be considered:

  • DbCtx - a DbContext type;
  • Entity - an entity type managed by instances of DbCtx;
  • EntityAuditTrigger - a registered trigger type that implements both IAfterSaveTrigger<Entity> and IBeforeCommitTrigger<Entity>;
  • TxInterceptor - an implementation of the IDbTransactionInterceptor interface.

All of the above are configured for use as follows, with services being an IServiceCollection instance:

services.AddDbContext<DbCtx>(dbOpts => dbOpts
   .UseSqlServer(...)
   .UseTriggers(trOpts => trOpts
       .UseTransactionalTriggers()
       .AddTrigger<EntityAuditTrigger>())
   .AddInterceptors(new TxInterceptor());

The following problems were found when attempting to fire the execution of transactional triggers from within the registered TxInterceptor instance:

  1. if transactional triggers are raised without there ever occurring a call to DbCtx#SaveChanges() or DbCtx#SaveChangesAsync(), an InvalidOperationException stating Trigger discovery process has not yet started. Please ensure that TriggerSession.DiscoverChanges() or TriggerSession.RaiseBeforeSaveTriggers() has been called is thrown during discovery of those triggers;
  2. EntityAuditTrigger#AfterSave() is invoked once for each instance of Entity that is managed during the execution of the transaction (as expected), but EntityAuditTrigger#BeforeCommit() is called, at most, a single time.

The first problem occurs because one of the discovery strategies in use, NonCascadingTriggerContextDiscoveryStrategy, always expects the existence of previously discovered changes. It is reasonable for transactional triggers to be raised without there ever being entity changes to detect, such as in the following cases:

  • an Exception is thrown before the first call to either DbCtx#SaveChanges() or DbCtx#SaveChangesAsync() is made during the transaction, and it is then rolled back;
  • a migration is being applied through the EntityFramework Core tools CLI.

Triggers implementing the following interface types are affected:

  • IBeforeCommitTrigger<>
  • IAfterCommitTrigger<>
  • IBeforeRollbackTrigger<>
  • IAfterRollbackTrigger

The exact cause of the second problem has yet to be determined. However, it is assumed that the proactive disposal of ITriggerSession instances handled by the library's TriggerSessionSaveChangesInterceptor type may pose an influence.

The problems described above were found with the following environmental setup:

  • EntityFramework Core 7
  • EntityFrameworkCore.Triggered 3.2.2

Trigger appears to be registered but not being called on BeforeSave

For some reason, my triggers have stopped working, even though nothing has changed in my Startup and DependencyInjection classes for quite some time.

Here is my DI code:

                services.AddDbContext<ApplicationDbContext>(options =>
                {
                    options.UseSqlServer(
                        configuration.GetConnectionString("DefaultConnection"),
                                b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName));

                    // Is there is a value for DNC_Seeding then we'll omit the triggers
                    var seeding = configuration["DNC_Seeding"];
                    if (string.IsNullOrEmpty(seeding))
                    {
                        // Normal operations - apply the trigger
                        eventLog.WriteEntry("Bookings2: Configuring Booking BeforeSave triggers", EventLogEntryType.Information, 2022, 10);
                        options.UseTriggers(triggerOptions => {
                            triggerOptions.AddTrigger<BookingsBeforeSaveTrigger>(ServiceLifetime.Singleton);
                        });
                    } else {
                        // Environment variable indicates we are seeding the database - omit for now
                    }
                });

I am logging to the Event Viewer and can see those entries in the log but when I save my Booking, the trigger doesn't seem to get fired.

I've tried this with and without a ServiceLifetime value but that doesn't seem to make any difference., except that it broke my unit tests so have since removed it.

Are there any diagnostics I can enable to determine where the problem lies?

Unable to use any serivces from trigger

Hi,
Triggers working great unless try to use any services. Services are working normally without any issues.
I have tried server configuration variations like:

services.AddDbContext/AddTriggeredDbContext(Options =>
{
Options.UseTriggers(triggerOptions =>
{
//triggerOptions.AddTrigger();
//triggerOptions.AddTrigger();
triggerOptions.AddAssemblyTriggers();

            });
        });

        
        //services.AddScoped<MHCDAL.Services.EmailServices.EmailSender>();
        services.AddScoped<MHCDAL.Services.EmailServices.IEmailSender, MHCDAL.Services.EmailServices.EmailSender>()

Trigger:

public class StockTrigger : IBeforeSaveTrigger, IAfterSaveTrigger
{
private readonly AppDbContext appDbContext;
private readonly IEmailSender emailSender;

    public StockTrigger(AppDbContext appDbContext, IEmailSender emailSender)
    {
        this.appDbContext = appDbContext;            
        this.emailSender = emailSender;            
    }

    public async Task BeforeSave(ITriggerContext<Stock> context, CancellationToken cancellationToken)
    {
        if (context.ChangeType != ChangeType.Deleted)
        {              
            await Task.Delay(1);
            Console.WriteLine(context.Entity.StockName);
        }
    }


    public async Task AfterSave(ITriggerContext<Stock> context, CancellationToken cancellationToken)
    {
        if (context.ChangeType != ChangeType.Deleted)
        {
              await emailSender.SendEmailAsync(emailSender.AdminEmail, "Test...i", context.Entity.StockName);
           
            Console.WriteLine(context.Entity.StockName);

        }
    }

}

Getting following error:
Screenshot 2021-07-31 085301

What I am missing?
Any help would be appreciated.

Thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.