Use PreContribute to fill OriginalValue for VO-derived members from ABP’s scalar PropertyChanges: PreContribute is not bringing any EntityChanges on the context. More assistance required.
Description
When implementing a custom IAuditLogContributor to support audit logging of ValueObjects, we discovered that the ABP Framework does not provide the correct InternalEntity on OriginalValues during the audit logging process while the NewValue is resolved correctly, the InternalEntity for original values is already overwritten by the time the contributor executes. All EF Core sources that should expose the previous state (e.g., EntityEntry.OriginalValues) already reflect the new value.
Technical Context
[AttributeUsage(AttributeTargets.Property)]
public class EntityChangeTrackedValueObjectAttribute : Attribute
{
public string MemberName { get; }
public bool IsMethod { get; }
public EntityChangeTrackedValueObjectAttribute(string memberName, bool isMethod = true)
{
MemberName = memberName;
IsMethod = isMethod;
}
}
*Custom AuditLogContributor* - Implemented so newValue is fetched
public override void PostContribute(AuditLogContributionContext context)
{
if (context?.AuditInfo?.EntityChanges == null)
{
base.PostContribute(context);
return;
}
foreach (var entityChange in context.AuditInfo.EntityChanges)
{
if (entityChange?.PropertyChanges == null) continue;
var entityTypeFullName = entityChange.EntityTypeFullName;
var entityTypeName = entityChange.EntityTypeFullName?.Split('.').LastOrDefault()?.Trim();
foreach (var propertyChange in entityChange.PropertyChanges)
{
if (propertyChange == null) continue;
var propertyPath = propertyChange.PropertyName ?? string.Empty;
if (string.IsNullOrWhiteSpace(propertyPath)) continue;
var propertyLast = propertyPath.Split('.').Last();
if (!string.IsNullOrEmpty(propertyChange.NewValue))
continue;
if (!string.IsNullOrEmpty(entityTypeName) &&
VoTrackedPropertyCache.TryGet(entityTypeName, propertyLast, out var voType, out var memberName, out var isMethod, out var declaringType))
{
var entryWrapper = entityChange.EntityEntry;
var wrapperType = entryWrapper.GetType();
object newResult = null;
var entityProp = wrapperType.GetProperty("Entity");
var currentEntity = entityProp?.GetValue(entryWrapper);
if (currentEntity != null && declaringType.IsInstanceOfType(currentEntity))
{
var voProp = declaringType.GetProperty(propertyLast);
var currentVoInstance = voProp?.GetValue(currentEntity);
if (currentVoInstance != null)
{
newResult = GetResultFromVo(currentVoInstance, voType, memberName, isMethod);
}
}
if (newResult != null)
{
propertyChange.NewValue = newResult.ToString();
propertyChange.PropertyTypeFullName = "System.String";
}
}
}
}
}
Observed Behavior
During the PostContribute phase, we attempted to retrieve the old values from: entityChange.EntityEntry, without success beecause all InternalEntities already ahd the new values.
Expected Behavior ABP should expose the actual original value of the Value Object within the audit context, when accessing the InternalEntity of a change.
So, we want to know if there is already in the framework a way to handle value objects obtaining similar results without using our implementation, and, if not, if you can provide a solution so we can use this aproach also for OriginalValues.
Thanks in advance
Hello. I was able to achieve it this way
/// Custom Menu Filter Component
/// IS TEMPORARY - TO BE REMOVED WHEN FIXED IN FUTURE RELEASE: https://abp.io/support/questions?sort=LastActivityDate-desc&creatorUserId=3a10d34c-87a0-a3f1-8e50-72cd7b2a718f&hidepinnedQuestions=True
@Component({
selector: 'app-custom-menu-filter',
imports: [CommonModule, FormsModule, LocalizationPipe],
standalone: true,
template: `
<div class="lpx-menu-filter">
<i class="bi bi-funnel menu-filter-icon"></i>
<!-- TODO: PROVIDE API -->
<input
class="menu-filter-input hidden-in-hover-trigger"
type="text"
[(ngModel)]="service.menuFilter"
[placeholder]="menuFilterText | abpLocalization"
/>
<span
(click)="service.clearFilter()"
class="menu-filter-clear hidden-in-hover-trigger"
[class.hidden]="!service.menuFilter"
>
<i class="bi bi-x clear-icon"></i>
</span>
</div>
`,
})
export class CustomMenuFilterComponent extends MenuFilterComponent {
override menuFilterText = 'LeptonX::FilterMenu' as SideMenuLayoutTranslate;
}
app.config.ts:
{
provide: CONTENT_BEFORE_ROUTES,
useValue: [CustomMenuFilterComponent],
multi: true,
},
lpx-menu-filter {
display: none !important;
}
sincelpx-menu-filter is not an ReplaceableComponent. If you can share a better wayu to achieve this I would appreciate it. Also I'll be waiting for feedback for when this issue is solved
Thanks, David Simões
Can you exemplify?
It is on angular and is page agnostic, it's on the sidebar layout
The text already exists on localization
The provided solution by @AI-Bot is not suficient
When ngx-datatable is in a loading state (initial load, sort, or external paging), the built-in loading/scroll indicator () is rendered outside the . As a result: The spinner/indicator appears below/above the table instead of centered inside the rows area. In some cases the “No data to display” label is still shown while loading.
Steps to Reproduce Create a simple page using ABP Extensible Table with externalPaging/externalSorting wired to an async ListService. Trigger a page change or sort so that the table enters loading state.
Observe the DOM: is appended as a sibling to (or otherwise outside the body/viewport), not inside it. Visually, the spinner sits outside the rows area;
Also when the number of results is still being calculated it display the text "NaN" instead of 0 as in previous versions
Yes. It is. I've solved it on my current implementation using datatable.recalculateColumns(). Is there a issue open on github?