Closed hassanrazakhalid closed 6 days ago
Could you clarify which property you don't see in EventEntry.Changes for your example? I couldn't understand the screenshot. Please provide what you are getting and what your expectation is
yes. What i think should be like this Changes structure similar to
{
"Changes": [
// ommiting other properities
{
"PropertyName": "PriceRanges"
"OldValue": "some-json-string",
"NewValue": "some-json-string"
}
]
}
since "PriceRanges" is defined as Owned and cannot exist independently, When i modify entity "ShipperPricing" and modify PriceRanges, then i expect to see is value changes along with non-complex properties. Right now
.AuditEntityAction<AuditLog>
is missing "PriceRanges" in changes when i save parent entity "ShipperPricing"
I was unable to reproduce. You should see the changes to the owned property by the owner's entity.
Could you provide a minimal project that replicates the issue, or at the very least, share all relevant code and configuration details?
Note you probably want to set the ReloadDatabaseValues
configuration to true, since you use DbContext.Update (see this)
For example:
Audit.EntityFramework.Configuration.Setup()
.ForAnyContext(cfg => cfg
.ReloadDatabaseValues());
Adding ReloadDatabaseValues() gives exception
An unhandled exception has occurred while executing the request.
System.Collections.Generic.KeyNotFoundException: The given key 'Property: SkuRangePricing.Id (no field, int) Shadow Required PK AfterSave:Throw ValueGenerated.OnAdd' was not present in the dictionary.
at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
at Microsoft.EntityFrameworkCore.Query.StructuralTypeProjectionExpression.BindProperty(IProperty property)
at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.BindProperty(StructuralTypeReferenceExpression typeReference, IProperty property)
at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TryBindMember(Expression source, MemberIdentity member, Expression& expression, IPropertyBase& property)
at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TryBindMember(Expression source, MemberIdentity member, Expression& expression)
at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlServerSqlTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
at Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlServerSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
at Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlServerSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.TranslateInternal(Expression expression, Boolean applyDefaultTypeMapping)
at Microsoft.EntityFrameworkCore.Query.RelationalSqlTranslatingExpressionVisitor.Translate(Expression expression, Boolean applyDefaultTypeMapping)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateExpression(Expression expression, Boolean applyDefaultTypeMapping)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateLambdaExpression(ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateWhere(ShapedQueryExpression source, LambdaExpression predicate)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.Translate(Expression expression)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.Translate(Expression expression)
at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass9_0`1.<Execute>b__0()
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
at Microsoft.EntityFrameworkCore.Internal.EntityFinder`1.GetDatabaseValues(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry.GetDatabaseValues()
at Audit.EntityFramework.DbContextHelper.CreateAuditEvent(IAuditDbContext context)
at Audit.EntityFramework.DbContextHelper.BeginSaveChangesAsync(IAuditDbContext context, CancellationToken cancellationToken)
at Audit.EntityFramework.AuditSaveChangesInterceptor.SavingChangesAsync(DbContextEventData eventData, InterceptionResult`1 result, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Diagnostics.Internal.SaveChangesInterceptorAggregator.CompositeSaveChangesInterceptor.SavingChangesAsync(DbContextEventData eventData, InterceptionResult`1 result, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
at Application.Features.Shippers.Commands.SaveShipperPricing.SaveShipperPricingCommandHandler.Handle(SaveShipperPricingCommand request, CancellationToken cancellationToken) in /home/hassan/projects/uruk/fulfillment-web/src/Fulfilment.Application/Features/Shippers/Commands/SaveShipperPricing/SaveShipperPricingCommandHandler.cs:line 62
at Application.Commons.Behaviors.ValidationBehavior`2.Handle(TRequest request, RequestHandlerDelegate`1 next, CancellationToken cancellationToken) in /home/hassan/projects/uruk/fulfillment-web/src/Fulfilment.Application/Commons/Behaviors/ValidationBehavior.cs:line 23
at Application.Commons.Behaviors.LoggingBehavior`2.Handle(TRequest request, RequestHandlerDelegate`1 next, CancellationToken cancellationToken) in /home/hassan/projects/uruk/fulfillment-web/src/Fulfilment.Application/Commons/Behaviors/LoggingBehavior.cs:line 25
at Fulfillment.Sys.Controllers.Shippers.ShipperController.SavePricingDetails(SaveShipperPricingCommand command) in /home/hassan/projects/uruk/fulfillment-web/src/Fulfillment.Sys/Controllers/Shippers/ShipperController.cs:line 261
at lambda_method487(Closure, Object)
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
ShipperPrice Configuration
public sealed class ShipperPricingConfigurationConfiguration : IAuditableEntityConfiguration<ShipperPricing>
{
public override void ConfigureEntity(EntityTypeBuilder<ShipperPricing> builder)
{
builder.Property(x => x.Id)
.ValueGeneratedNever();
builder.Property(x => x.OfferedService)
.HasConversion<string>()
.HasMaxLength(StringLength.StandardMax);
// builder.Property(x => x.SkuRanges)
//
// .HasMaxLength(StringLength.StandardMax);
builder.OwnsMany(x => x.PriceRanges, skuRange =>
{
skuRange.ToJson();
skuRange.Property(x => x.Fee)
.HasColumnType("decimal(9, 2)");
});
builder.Property(x => x.ExtraPerSku)
.HasColumnType("decimal(9, 2)");
}
}
The configuration of Audit.Net i'm using
Audit.EntityFramework.Configuration.Setup()
.ForContext<AppDbContext>(config => config
.IncludeEntityObjects()
.ForEntity<ShipperPricing>(_ => _
.Ignore(x => x.UpdatedAt)
.Override(x => x.PriceRanges, value =>
{
return value.Entity as List<ShipperPricing>;
})
.Format(x => x.PriceRanges, value =>
{
return JsonSerializer.Serialize(value);
})
)
// .ForEntity<ShipperPricing>(_ => _
// .Format(sp => sp.PriceRanges, pr => JsonSerializer.Serialize(pr))))
)
.UseOptOut()
;
Audit.Core.Configuration.Setup()
.UseEntityFramework(ef => ef
.AuditTypeExplicitMapper(m => m
.Map<Shipper, AuditLog>((shipper, entry, auditLog) =>
{
auditLog.TableName = nameof(AppDbContext.Shippers);
return true;
})
.Map<ShipperCarrier, AuditLog>((shipper, entry, auditLog) =>
{
auditLog.TableName = nameof(AppDbContext.ShipperCarriers);
auditLog.Reference.Add((entry.GetEntry().Entity as ShipperCarrier)!.ShipperId.ToString());
return true;
})
// .Map<SkuRangePricing, AuditLog>((shipper, entry, auditLog) =>
// {
// return true;
// })
.Map<ShipperPricing, AuditLog>((shipper, entry, auditLog) =>
{
auditLog.TableName = nameof(AppDbContext.ShipperPricings);
auditLog.Reference.Add((entry.GetEntry().Entity as ShipperPricing)!.ShipperId.ToString());
return true;
})
.AuditEntityAction<AuditLog>((ev, entry, entity) =>
{
// To fix audit infinity cycle in case of DbUpdateException
if (ev.Environment.Exception != null)
{
return false;
}
// If only created at or updated at is modified and not prop is change ignoring that event
if (entry.GetEntry().Entity is IAuditableEntity auditableEntity)
{
if (entry.Changes == null || !entry.Changes.Any()) return false;
if (entry.Changes != null && entry.Changes.Count == 1)
{
if (entry.Changes.First().ColumnName == nameof(IAuditableEntity.CreatedAt) ||
entry.Changes.First().ColumnName == nameof(IAuditableEntity.UpdatedAt))
{
return false;
}
}
}
entity.Action = entry.Action;
entity.Exception = ev.GetEntityFrameworkEvent().ErrorMessage;
entity.Data = entry.ToJson();
entity.Changes = JsonSerializer.Serialize(entry.Changes ?? []);
entity.EntityType = entry.EntityType.Name;
entity.AuditDate = DateTime.UtcNow;
entity.TablePk = entry.PrimaryKey.First().Value.ToString()!;
var userId = ev.CustomFields[AuditKeys.UserId];
var email = ev.CustomFields[AuditKeys.Email];
if (userId is not null)
{
entity.ByUserId = userId.ToString()!;
entity.ByUserName = email.ToString()!;
// ev.CustomFields["User"] = userId;
}
return true;
}))
.IgnoreMatchedProperties(true));
I have a few questions
// .Map<SkuRangePricing, AuditLog>((shipper, entry, auditLog) =>
// {
// return true;
// })
I'm sharing my findings, may be it will give you a clue where things are going wrong. If still, a minimal project is required. Let me know.
The Override
and Format
methods are not applied to navigation properties (or owned/complex properties of the owner entity) because they rely on EF Core change tracker entries. In your scenario, the EF change tracker will create separate entries for ShipperPricing
and SkuRangePricing
, even though the latter is defined as an owned entity in the same table. Internally, EF treats them as distinct entities.
The Map<SkuRangePricing, AuditLog>
action is executed for the same reason.
To address the issue mentioned in your comment // To fix audit infinity cycle in case of DbUpdateException
, you can resolve it by using a separate instance of your DbContext.
You can invoke UseDbContext
to define a new DbContext instance like this:
Audit.Core.Configuration.Setup()
.UseEntityFramework(ef => ef
.UseDbContext(ctx => ctx.GetEntityFrameworkEvent().GetDbContext().GetService<AppDbContext>())
);
Ultimately, the solution depends on your specific implementation and requirements, which are not entirely clear. If you need further assistance, please provide a minimal sample project that reproduces the issue, along with details about your expected audit output.
Thought I'd piggy back on this open issue instead of opening another, very similar one.
@thepirat000 Is there no way to bundle parent and owned entities together in the one AuditEvent output?
So, for example, let's say I have a Customer entity which has a "PrimaryAddress" and "SecondaryAddress" both of which are type "CustomerAddress" which itself is a Value Object (C# record):
Updating the addresses will produce a DELETE and INSERT record in the audit_logs
table for the changes, and there are no entries in the Customer Changes[]
array. This is true even if there are no changes to the address. A DELETE and INSERT will still be added to the audit_logs
table even if changes are only made to the parent entity.
Guessing there is no way to have the owned entities also show up in the Changes[]
array? It's slightly problematic because the entity types in the audit_logs
entity_type
column are different. The customer is Customer
and the addresses are CustomerAddress
, which means querying the audit log for reconstruction and displaying on the FE becomes cumbersome depending on how many owned entities there are.
Cheers for any clarification!
Actually, the parent and owned entities modified together are included in the same AuditEvent. I believe you mean having the entries grouped within the same audit event.
There isn't an automatic built-in way to achieve this, but you can always manipulate the output with a custom action, for example:
using Audit.EntityFramework;
Audit.Core.Configuration.AddOnCreatedAction(scope =>
{
var efEvent = scope.GetEntityFrameworkEvent();
efEvent.Entries = ReArrangeEntries(efEvent.Entries);
});
Actually, the parent and owned entities modified together are included in the same AuditEvent. I believe you mean having the entries grouped within the same audit event.
There isn't an automatic built-in way to achieve this, but you can always manipulate the output with a custom action, for example:
using Audit.EntityFramework; Audit.Core.Configuration.AddOnCreatedAction(scope => { var efEvent = scope.GetEntityFrameworkEvent(); efEvent.Entries = ReArrangeEntries(efEvent.Entries); });
My apologies, you're right. Thank you, I'll explore this further :)
Describe the bug Owned entities when mapped to Json not showing in EventEntry.Changes callback. Is there a way to override changes implementation or wo could a json in oldValue and newValue for complex types.
To Reproduce
Expected behavior OwnedEntities should be visible in Changes.
Libraries (specify the Audit.NET extensions being used including version): For example:
Target .NET framework: For example:
Additional context
See the below screenshot. Are owned entries should be treated as separate entries. Also the state is Added during execution, which is why i guess its not added to value changes