Base solution for your next web application
Open Closed

Entity History #5176


User avatar
0
BobIngham created

dotnet-core, angular, 5.4.1 I implement Entity History in ProjectnameEntityFrameworkCoreModule (I am tracking history for all fully audited entities):

Configuration.EntityHistory.IsEnabled = true; //Enable this to write change logs for the entities below:
Configuration.EntityHistory.Selectors.Add("NuagecareEntities", typeof(OrganizationUnit), typeof(Role), typeof(User), typeof(Tenant));
Configuration.EntityHistory.Selectors.Add(
    new NamedTypeSelector(
        "Abp.FullAuditedEntities",
        type => typeof(IFullAudited).IsAssignableFrom(type)
     ));

In my system a tenant is able to implement a data provider and the system will use Hangfire to invoke a background job to get data changes made in the data provider database. My entity (NcEntity) is made up of two properties, DisplayName and ExtensionData. The latter is a collection of properties in Json format implemented with the IExtendableObject interface. My DataProviderAPIService class receives data from the data provider, deserialises it and carries out an update, insert or soft-delete, the code below shows the update method, code simplified:

if (ncEntity.DisplayName != entity.DisplayName || ncEntity.ExtensionData != entity.ExtensionData)
{
    ncEntity.DisplayName = entity.DisplayName;
    ncEntity.ExtensionData = entity.ExtensionData;
}
_ncEntityRepository.Update(ncEntity);

This works aok in terms of updating data but it does not invoke entity history, I have no entry in the entity history tables. If I use the user interface and edit an entity by, for example, changing the DisplayName property the change is captured in entity history. I have read the documentation at [https://aspnetboilerplate.com/Pages/Documents/Entity-History]) but can find no pointers as to where i am going wrong. The question is:

How do I implement entity history in a ServiceAPI?


16 Answer(s)
  • User Avatar
    0
    aaron created
    Support Team

    Show the relevant code. Why are there ncEntity and entity? If ncEntity was untracked, then isModified = !(OriginalValue.Equals(CurrentValue)) would be false.

  • User Avatar
    0
    BobIngham created

    Hi Aaron, Thanks for the reply. entity is returned from my data provider as a list and then and serialized as the following Dto:

    public class DataProviderEntityDto : EntityDto<Guid>
    {
        public string DisplayName { get; set; }
    
        public string ExtensionData { get; set; }
    }
    

    NcEntity is a domain entity defined as below:

    [Table("NcEntity")]
    public class NcEntity : FullAuditedEntity<Guid>, IMustHaveTenant, IMayHaveOrganizationUnit, IExtendableObject
    {
        public int TenantId { get; set; }
    
        public long? OrganizationUnitId { get; set; }
    
        [Required]
        [StringLength(DbConsts.MaxNameLength)]
        public virtual string DisplayName { get; set; }
    
        public virtual string ExtensionData { get; set; }
    }
    

    My DataProviderAPIService reads the third party database and returns a list of entities. If either value (DisplayName or ExtensionData) in the entity is different to the relevant value in the NcEntity domain object the system updates NcEntity. NcEntity is a fully audited entity and I'm not sure why the history is not captured. I am not sure what you mean by >If ncEntity was untracked, then isModified = !(OriginalValue.Equals(CurrentValue)) would be false. . Thanks for your help so far.

  • User Avatar
    0
    aaron created
    Support Team

    <cite>aaron: </cite> Show the relevant code.

    If you're not sure what's relevant, show the entire method.

  • User Avatar
    0
    BobIngham created

    Thanks for staying with me Aaron, here's the full method which is a work in progress.

    [UnitOfWork]
    private DataProviderUpdateTotals UpdateEntities(List<DataProviderEntityDto> entities, int tenantId)
    {
        var totals = new DataProviderUpdateTotals(0, 0, 0);
    
        if (entities != null)
        {
            using (_abpSession.Use(tenantId, null))
            {
    
                foreach (var entity in entities)
                {
                    var dataProviderEntityAttributes = JsonConvert.DeserializeObject<Dictionary<string, string>>(entity.ExtensionData);
                    var dataProviderId = dataProviderEntityAttributes
                        .Where(m => m.Key == "DataProviderId")
                        .Select(m => m.Value)
                        .FirstOrDefault();
    
                    //error exceptions due to duplicatedResidentId's in ResiData
                    var ncEntityDataProviderIdXRef = new NcEntityDataProviderIdXRef();
                    var ncEntity = new NcEntity.NcEntity();
                    try
                    {
                        ncEntityDataProviderIdXRef = _ncEntityDataProviderIdXRefRepository.FirstOrDefault(m => m.DataProviderId == Convert.ToInt32(dataProviderId));
                        if (ncEntityDataProviderIdXRef == null)
                        {
                            //add new entity...
                            var organizationUnitId = _organizationUnitAttributeRepository.FirstOrDefault(m => m.Value == entity.OrganisationalUnitXRef).OrganizationUnitId;
                            CreateAndSaveNcEntity(entity, tenantId, organizationUnitId).GetAwaiter().GetResult();
                            totals.EntitiesAdded++;
                            break;
                        }
                    }
                    catch (Exception ex)
                    {
                        Logger.Error("Error in ncEntityDataProviderIdXRef during DataProviderAPIProvider.UpdateEntities, values: TenantId; " + tenantId.ToString() + " DataProviderId;" + dataProviderId.ToString(), ex);
                        break;
                    }
    
                    try
                    {
                        ncEntity = _ncEntityRepository.Get(ncEntityDataProviderIdXRef.NcEntityId);
                    }
                    catch (Exception ex)
                    {
                        Logger.Error("Duplicate in ncEntity during DataProviderAPIProvider.UpdateEntities, values: TenantId; " + tenantId.ToString() + " DataProviderId;" + dataProviderId.ToString(), ex);
                        break;
                    }
    
                    var systemOrganizationUnit = _organizationUnitAttributeRepository.FirstOrDefault(m => m.OrganizationUnitId == ncEntity.OrganizationUnitId).Value;
    
                    if (ncEntity.DisplayName != entity.DisplayName || ncEntity.ExtensionData != entity.ExtensionData || entity.OrganisationalUnitXRef != systemOrganizationUnit)
                    {
                        ncEntity.DisplayName = entity.DisplayName;
                        ncEntity.ExtensionData = entity.ExtensionData;
                        totals.EntitiesUpdated++;
    
                        if (entity.OrganisationalUnitXRef != systemOrganizationUnit)
                        {
                            var newEntityOrganizationUnit = _organizationUnitAttributeRepository.FirstOrDefault(m => m.Value == entity.OrganisationalUnitXRef).OrganizationUnitId;
                            if (newEntityOrganizationUnit != 0)
                            {
                                ncEntity.OrganizationUnitId = newEntityOrganizationUnit;
                            }
                            else //TODO
                            {
                                //if home doesn't exist create it
                                //if location doesn't exist create it
                                //add the entity to the new location
                                //ncEntity.OrganizationUnitId = newEntityOrganizationUnit;
                            }
                            totals.EntitiesMoved++;
                        }
                        _ncEntityRepository.Update(ncEntity);
                    }
                }
            }
        }
        return totals;
    }
    
  • User Avatar
    0
    BobIngham created

    Hi Aaron, Any thoughts or pointers on this?

  • User Avatar
    0
    aaron created
    Support Team

    Looked at it several times. Did you modify _ncEntityRepository.Get?

  • User Avatar
    0
    BobIngham created

    Hi Aaron, I think you overestimate my programming skills - I wouldn't have a clue how to do that! I go to an initial trial later this week for a fortnight or so so I would like to have some idea of what to do because the audit trail is a major USP, what with GDPR and all that. Is your source code embedded in a DLL or can I follow it through by placing a break point in Zero somewhere?

  • User Avatar
    0
    aaron created
    Support Team

    Try commenting out _ncEntityRepository.Update(ncEntity);

    Is your source code embedded in a DLL or can I follow it through by placing a break point in Zero somewhere?

    All official ASP.NET Boilerplate NuGet packages are Sourcelink-enabled: <a class="postlink" href="https://aspnetboilerplate.com/Pages/Documents/Debugging">https://aspnetboilerplate.com/Pages/Documents/Debugging</a>

  • User Avatar
    0
    BobIngham created

    Hi Aaron, I tried exactly as you said and a couple of other things to no avail. However, on closer inspection I noticed your entity history tables were full of non-nullable UserId's. I changed this line to force UserId = 1 as the standard host admin:

    using (_abpSession.Use(tenantId, 1))
    {...
    

    <ins>AbpEntityChangeSets</ins> That now gives me a record in AbpEntityChangeSets with a null value in BrowserInfo, ClientIpAddress, _ClientName_and Reason(as can be expected). I think I should be able to populate the Reason column using IEntityChangeSetReasonProvider, I'm not sure how I could insert values for ClientIpAddress and ClientName, which could be useful. <ins>AbpEntityChanges</ins> I have a full set of data in AbpEntityChanges which is great. <ins>AbpEntityPropertyChanges</ins> Herein lies the problem. The AbpEntityPropertyChanges has not been populated at all which kind of makes the other two entries pointless in that we know something has changed but we don't know what.

    Can you help me track the problem a little bit further? Sorry to be a pain but hopefully the above should give a few pointers?

  • User Avatar
    0
    alper created
    Support Team

    All these extra information (BrowserInfo, ClientIpAddress, ClientName and Reason etc..) is being saved automatically

    <a class="postlink" href="https://github.com/aspnetboilerplate/aspnetboilerplate/blob/86430bd4366124d7dc98c04684b0e529634d314d/src/Abp.ZeroCore.EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs#L74">https://github.com/aspnetboilerplate/as ... per.cs#L74</a>

  • User Avatar
    0
    BobIngham created

    @alper, thanks for that, I can live with null values in the AbpEntityChangeSets.BrowserInfo, ClientIpAddress and ClientName columns. But it still doesn't answer why I have no persistence in the AbpEntityPropertyChanges table. @aaron - any ideas how to address this?

  • User Avatar
    0
    aaron created
    Support Team

    Try _unitOfWorkManager.Current.SaveChanges(); just before return totals;

  • User Avatar
    0
    BobIngham created

    _unitOfWorkManager.Current.SaveChanges() throws a null error on .Current. I am kind of tearing my hair out with this one but entity history does not form a part of what I need to move the system to trial next week. I will put this on one side and come back to it. In the meantime I have attached my DataProvider class, the method "UpdateFromDataProvider" is called direct from Hangfire from the following worker class:

    using Abp.BackgroundJobs;
    using Abp.Configuration;
    using Abp.Dependency;
    using Abp.Domain.Repositories;
    using Nuagecare.App.Services;
    using Nuagecare.MultiTenancy;
    using System.Linq;
    
    namespace Nuagecare.Web.Hangfire.Workers
    {
        public class UpdateFromDataProvider : BackgroundJob<int>, ITransientDependency
        {
            private readonly ITenantAppService _tenantService;
            private readonly IRepository<Tenant> _tenantRepository;
            private readonly SettingManager _settingManager;
            private readonly DataProviderAPIProvider _dataProviderAPIProvider;
    
            public UpdateFromDataProvider(
                ITenantAppService tenantService,
                IRepository<Tenant> tenantRepository,
                SettingManager settingManager,
                DataProviderAPIProvider dataProviderAPIProvider)
            {
                _tenantService = tenantService;
                _tenantRepository = tenantRepository;
                _settingManager = settingManager;
                _dataProviderAPIProvider = dataProviderAPIProvider;
            }
    
            public override void Execute(int number)
            {
                var tenants = _tenantRepository.GetAllList()
                    .Where(t => t.IsActive == true);
    
                foreach (var tenant in tenants)
                {
                    _dataProviderAPIProvider.UpdateFromDataProvider(tenant.Id);
                }
                return;
            }
        }
    }
    

    I am hoping you can throw some light on why I am getting a row in each of AbpEntityChangeSets and AbpEntityChanges but nothing in AbpEntityPropertyChanges.

    Cheers, Bob

    [attachment=0:3cw44pfw]DataProviderAPIProvider.zip[/attachment:3cw44pfw] DataProviderAPIProvider.zip

  • User Avatar
    0
    aaron created
    Support Team

    Make your method protected virtual:

    [UnitOfWork]
    protected virtual DataProviderUpdateTotals UpdateEntities(List<DataProviderEntityDto> entities, int tenantId)
    

    Why: UnitOfWork Attribute Restrictions

  • User Avatar
    0
    BobIngham created

    That's taken me one step backwards, now there are no entity history records at all. I think the only thing to do here is for me to build a base Zero aspnet-core project and import a text file using this code, fired from Hangfire and then we can take a look again. At the moment I get the feeling we are applying hacks and not knowing why it is not working. For example, why would the .Current in _unitOfWorkManager.Current.SaveChanges(); return null?

  • User Avatar
    0
    aaron created
    Support Team

    For example, why would the .Current in _unitOfWorkManager.Current.SaveChanges(); return null?

    Because there's no Unit of Work.