I was able to fix this myself by replacing the OrganizationUnitManager
service and override the MoveAsync
method so that it calledOrganizationUnitRepository.UpdateAsync
after making any changes to properties of any OrganizationUnit
.
One problem I had, was that I couldn't readily access the OrganizationUnit.Code
or OrganizationUnit.ParentId
property setters, as they were marked as internal. I had to use reflection to set the properties to combine the existing method functionaliy with the fix.
I'm not sure how it worked for you, as this layer of code is not database specific, and I wouldn't have expected it to work for Entity Framework either.
Check the docs before asking a question: https://docs.abp.io/en/commercial/latest/ Check the samples, to see the basic tasks: https://docs.abp.io/en/commercial/latest/samples/index The exact solution to your question may have been answered before, please use the search on the homepage.
The role claims are not being saved as the UpdateClaimsAsync()
method within the IdentityRoleAppService
class is missing a await RoleRepository.UpdateAsync(role);
at the end.
I was able to fix it by replacing the IdentityRoleAppService
and appending the missing line in the overriden method.
[15:06:22 ERR] Error occured while adding the module Volo.Identity.Prowith source-code to the solution "2ae0add0-a136-48b3-889f-e331d74b2f66".. System.InvalidOperationException: Sequence contains no matching element
at System.Linq.ThrowHelper.ThrowNoMatchException()
at System.Linq.Enumerable.First[TSource](IEnumerable1 source, Func
2 predicate)
at Volo.Abp.Cli.ProjectModification.SolutionModuleAdder.ChangeDomainTestReferenceToMongoDB(ModuleWithMastersInfo module, String moduleSolutionFile) in D:\github\abp\framework\src\Volo.Abp.Cli.Core\Volo\Abp\Cli\ProjectModification\SolutionModuleAdder.cs:line 201
at Volo.Abp.Cli.ProjectModification.SolutionModuleAdder.RemoveUnnecessaryProjectsAsync(String solutionDirectory, ModuleWithMastersInfo module, String[] projectFiles) in D:\github\abp\framework\src\Volo.Abp.Cli.Core\Volo\Abp\Cli\ProjectModification\SolutionModuleAdder.cs:line 151
at Volo.Abp.Cli.ProjectModification.SolutionModuleAdder.AddAsync(String solutionFile, String moduleName, String startupProject, String version, Boolean skipDbMigrations, Boolean withSourceCode, Boolean addSourceCodeToSolutionFile, Boolean newTemplate, Boolean newProTemplate) in D:\github\abp\framework\src\Volo.Abp.Cli.Core\Volo\Abp\Cli\ProjectModification\SolutionModuleAdder.cs:line 98
at Volo.Abp.Cli.Commands.AddModuleCommand.ExecuteAsync(CommandLineArgs commandLineArgs) in D:\github\abp\framework\src\Volo.Abp.Cli.Core\Volo\Abp\Cli\Commands\AddModuleCommand.cs:line 59
at Volo.Abp.Suite.Controllers.AbpSuiteController.AddModuleAsync(AddModuleInput input)
[15:06:38 ERR] Error occured while adding the module Volo.Identity.Prowith source-code to the solution "2ae0add0-a136-48b3-889f-e331d74b2f66".. System.InvalidOperationException: Sequence contains no matching element
at System.Linq.ThrowHelper.ThrowNoMatchException()
at System.Linq.Enumerable.First[TSource](IEnumerable1 source, Func
2 predicate)
at Volo.Abp.Cli.ProjectModification.SolutionModuleAdder.ChangeDomainTestReferenceToMongoDB(ModuleWithMastersInfo module, String moduleSolutionFile) in D:\github\abp\framework\src\Volo.Abp.Cli.Core\Volo\Abp\Cli\ProjectModification\SolutionModuleAdder.cs:line 201
at Volo.Abp.Cli.ProjectModification.SolutionModuleAdder.RemoveUnnecessaryProjectsAsync(String solutionDirectory, ModuleWithMastersInfo module, String[] projectFiles) in D:\github\abp\framework\src\Volo.Abp.Cli.Core\Volo\Abp\Cli\ProjectModification\SolutionModuleAdder.cs:line 151
at Volo.Abp.Cli.ProjectModification.SolutionModuleAdder.AddAsync(String solutionFile, String moduleName, String startupProject, String version, Boolean skipDbMigrations, Boolean withSourceCode, Boolean addSourceCodeToSolutionFile, Boolean newTemplate, Boolean newProTemplate) in D:\github\abp\framework\src\Volo.Abp.Cli.Core\Volo\Abp\Cli\ProjectModification\SolutionModuleAdder.cs:line 98
at Volo.Abp.Cli.Commands.AddModuleCommand.ExecuteAsync(CommandLineArgs commandLineArgs) in D:\github\abp\framework\src\Volo.Abp.Cli.Core\Volo\Abp\Cli\Commands\AddModuleCommand.cs:line 59
at Volo.Abp.Suite.Controllers.AbpSuiteController.AddModuleAsync(AddModuleInput input)
[15:07:27 ERR] Error occured while adding the module Volo.AuditLogging.Uiwith source-code to the solution "2ae0add0-a136-48b3-889f-e331d74b2f66".. System.ArgumentNullException: Value cannot be null. (Parameter 'path')
at System.IO.Path.GetFullPath(String path)
at System.IO.DirectoryInfo..ctor(String path)
at Volo.Abp.Cli.ProjectModification.SolutionModuleAdder.RemoveProjectByPostFix(ModuleWithMastersInfo module, String moduleSolutionFile, String targetFolder, String postFix) in D:\github\abp\framework\src\Volo.Abp.Cli.Core\Volo\Abp\Cli\ProjectModification\SolutionModuleAdder.cs:line 186
at Volo.Abp.Cli.ProjectModification.SolutionModuleAdder.RemoveUnnecessaryProjectsAsync(String solutionDirectory, ModuleWithMastersInfo module, String[] projectFiles) in D:\github\abp\framework\src\Volo.Abp.Cli.Core\Volo\Abp\Cli\ProjectModification\SolutionModuleAdder.cs:line 150
at Volo.Abp.Cli.ProjectModification.SolutionModuleAdder.AddAsync(String solutionFile, String moduleName, String startupProject, String version, Boolean skipDbMigrations, Boolean withSourceCode, Boolean addSourceCodeToSolutionFile, Boolean newTemplate, Boolean newProTemplate) in D:\github\abp\framework\src\Volo.Abp.Cli.Core\Volo\Abp\Cli\ProjectModification\SolutionModuleAdder.cs:line 98
at Volo.Abp.Cli.Commands.AddModuleCommand.ExecuteAsync(CommandLineArgs commandLineArgs) in D:\github\abp\framework\src\Volo.Abp.Cli.Core\Volo\Abp\Cli\Commands\AddModuleCommand.cs:line 59
at Volo.Abp.Suite.Controllers.AbpSuiteController.AddModuleAsync(AddModuleInput input)
I am using MongoDB
I tried to add the Identity and Audit Logging modules through the abp suite for an existing solution.
If anyone else is interested in doing this, here is my own implementation to add some simple entity change tracking by overriding the default MongoDbRepository class.
public class MongoDbEntityChangeRepository<TMongoDbContext, TEntity, TKey>
: MongoDbRepository<TMongoDbContext, TEntity, TKey>
, IAuditedRepository<TEntity, TKey>
where TMongoDbContext : IAbpMongoDbContext
where TEntity : class, IEntity<TKey>
{
public MongoDbEntityChangeRepository(IMongoDbContextProvider<TMongoDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
private readonly object _serviceProviderLock = new object();
private TRef LazyGetRequiredService<TRef>(Type serviceType, ref TRef reference)
{
if (reference == null)
{
lock (_serviceProviderLock)
{
if (reference == null)
{
reference = (TRef)ServiceProvider.GetRequiredService(serviceType);
}
}
}
return reference;
}
public EntityChangeLogger EntityChangeLogger => LazyGetRequiredService(typeof(EntityChangeLogger), ref _entityChangeLogger);
private EntityChangeLogger _entityChangeLogger;
/// <summary>
/// Use this method to prevent fetching the entity again, if you already have it
/// </summary>
/// <param name="original"></param>
/// <param name="updateEntity"></param>
/// <param name="autoSave"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<TEntity> UpdateAsync(
TEntity original,
Action<TEntity> updateEntity,
bool autoSave = false,
CancellationToken cancellationToken = default(CancellationToken))
{
var previous = EntityChangeLogger.CloneEntity<TEntity, TKey>(original);
updateEntity(original);
var updated = original;
updated = await base.UpdateAsync(updated, autoSave, cancellationToken);
EntityChangeLogger.LogEntityUpdated<TEntity, TKey>(previous, updated);
return updated;
}
public override async Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = new CancellationToken())
{
TEntity previous = null;
bool entityHistoryEnabled = EntityChangeLogger.IsEntityHistoryEnabled<TEntity, TKey>();
if (entityHistoryEnabled)
{
previous = await GetAsync(entity.Id, cancellationToken: cancellationToken);
}
var result = await base.UpdateAsync(entity, autoSave, cancellationToken);
if (entityHistoryEnabled)
{
EntityChangeLogger.LogEntityUpdated<TEntity, TKey>(previous, entity);
}
return result;
}
public override async Task DeleteAsync(
TEntity entity,
bool autoSave = false,
CancellationToken cancellationToken = default)
{
await base.DeleteAsync(entity, autoSave, cancellationToken);
EntityChangeLogger.LogEntityDeleted<TEntity, TKey>(entity);
}
public override async Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = new CancellationToken())
{
var result = await base.InsertAsync(entity, autoSave, cancellationToken);
EntityChangeLogger.LogEntityCreated<TEntity, TKey>(result);
return result;
}
}
public class EntityChangeLogger
{
private readonly IAuditingManager _auditingManager;
private readonly IClock _clock;
private readonly IAuditingHelper _auditingHelper;
public EntityChangeLogger(IAuditingManager auditingManager, IClock clock, IAuditingHelper auditingHelper)
{
_auditingManager = auditingManager;
_clock = clock;
_auditingHelper = auditingHelper;
}
public void LogEntityCreated<T, TKey>(T current) where T : class, IEntity<TKey>
{
var entityType = typeof(T);
if (!_auditingHelper.IsEntityHistoryEnabled(entityType))
return;
_auditingManager.Current.Log.EntityChanges.Add(new EntityChangeInfo()
{
ChangeTime = _clock.Now,
ChangeType = EntityChangeType.Created,
EntityId = GetEntityId(current),
EntityTenantId = null,
EntityTypeFullName = entityType.FullName,
PropertyChanges = GetPropertyChanges<T, TKey>(null, current, true)
});
}
public void LogEntityDeleted<T, TKey>(T previous) where T : class, IEntity<TKey>
{
var entityType = typeof(T);
if (!_auditingHelper.IsEntityHistoryEnabled(entityType))
return;
_auditingManager.Current.Log.EntityChanges.Add(new EntityChangeInfo()
{
ChangeTime = _clock.Now,
ChangeType = EntityChangeType.Deleted,
EntityId = GetEntityId(previous),
EntityTenantId = null,
EntityTypeFullName = entityType.FullName,
PropertyChanges = GetPropertyChanges<T, TKey>(previous, null, true)
});
}
public bool IsEntityHistoryEnabled<T, TKey>() where T : class, IEntity<TKey>
{
var entityType = typeof(T);
return _auditingHelper.IsEntityHistoryEnabled(entityType);
}
public void LogEntityUpdated<T, TKey>(T previous, T current) where T : class, IEntity<TKey>
{
var entityType = typeof(T);
if (!IsEntityHistoryEnabled<T, TKey>())
return;
var notNullEntity = GetNotNull(previous, current);
_auditingManager.Current.Log.EntityChanges.Add(new EntityChangeInfo()
{
ChangeTime = _clock.Now,
ChangeType = EntityChangeType.Updated,
EntityId = GetEntityId(notNullEntity),
EntityTenantId = null,
EntityTypeFullName = entityType.FullName,
PropertyChanges = GetPropertyChanges<T, TKey>(previous, current, false)
});
}
private static ConcurrentDictionary<Type, PropertyInfos> _propertyInfoCache = new ConcurrentDictionary<Type, PropertyInfos>();
private class PropertyInfos
{
public PropertyInfos(PropertyInfo id, PropertyInfo extraProperties)
{
Id = id;
ExtraProperties = extraProperties;
}
public PropertyInfo Id { get; private set; }
public PropertyInfo ExtraProperties { get; private set; }
}
public T CloneEntity<T, TKey>(T entity) where T : class, IEntity<TKey>
{
// create a clone, without going back to the database again
var doc = JsonSerializer.Serialize(entity);
var clone = JsonSerializer.Deserialize<T>(doc);
// set protected properties
// - Id
// - Extra Properties
var entityType = typeof(T);
var propertyInfo = _propertyInfoCache.GetOrAdd(entityType, CreatePropertyInfos<TKey>);
propertyInfo.Id.SetValue(clone, entity.Id);
if (entityType.IsAssignableTo<IHasExtraProperties>())
{
IHasExtraProperties e = (IHasExtraProperties)entity;
propertyInfo.ExtraProperties.SetValue(clone, SimpleClone(e.ExtraProperties));
}
return clone;
}
private PropertyInfos CreatePropertyInfos<TKey>(Type entityType)
{
return new PropertyInfos(
entityType.GetProperty(nameof(IEntity<TKey>.Id)),
entityType.GetProperty(nameof(IHasExtraProperties.ExtraProperties)));
}
private T SimpleClone<T>(T toClone)
{
var doc = JsonSerializer.Serialize(toClone);
return JsonSerializer.Deserialize<T>(doc);
}
private List<EntityPropertyChangeInfo> GetPropertyChanges<T, TKey>(T previous, T current, bool logIfNoChange) where T : class, IEntity<TKey>
{
List<EntityPropertyChangeInfo> result = new List<EntityPropertyChangeInfo>();
foreach (var propertyInfo in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (ShouldSavePropertyHistory(previous, current, propertyInfo, logIfNoChange, out string previousValue, out string currentValue))
{
result.Add(new EntityPropertyChangeInfo
{
NewValue = currentValue,
OriginalValue = previousValue,
PropertyName = propertyInfo.Name,
PropertyTypeFullName = propertyInfo.PropertyType.FullName
});
}
}
return result;
}
private bool ShouldSavePropertyHistory<T>(T previous, T current, PropertyInfo propertyInfo, bool logIfNoChange, out string previousValue, out string currentValue)
{
previousValue = null;
currentValue = null;
if (propertyInfo.IsDefined(typeof(DisableAuditingAttribute), true))
{
return false;
}
var entityType = typeof(T);
if (entityType.IsDefined(typeof(DisableAuditingAttribute), true))
{
if (!propertyInfo.IsDefined(typeof(AuditedAttribute), true))
{
return false;
}
}
if (IsBaseAuditProperty(propertyInfo, entityType))
{
return false;
}
previousValue = previous != null ? JsonSerializer.Serialize(propertyInfo.GetValue(previous)).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength) : null;
currentValue = current != null ? JsonSerializer.Serialize(propertyInfo.GetValue(current)).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength) : null;
var isModified = !(previousValue?.Equals(currentValue) ?? currentValue == null);
if (isModified)
{
return true;
}
return logIfNoChange;
}
private bool IsBaseAuditProperty(PropertyInfo propertyInfo, Type entityType)
{
if (entityType.IsAssignableTo<IHasCreationTime>()
&& propertyInfo.Name == nameof(IHasCreationTime.CreationTime))
{
return true;
}
if (entityType.IsAssignableTo<IMayHaveCreator>()
&& propertyInfo.Name == nameof(IMayHaveCreator.CreatorId))
{
return true;
}
if (entityType.IsAssignableTo<IMustHaveCreator>()
&& propertyInfo.Name == nameof(IMustHaveCreator.CreatorId))
{
return true;
}
if (entityType.IsAssignableTo<IHasModificationTime>()
&& propertyInfo.Name == nameof(IHasModificationTime.LastModificationTime))
{
return true;
}
if (entityType.IsAssignableTo<IModificationAuditedObject>()
&& propertyInfo.Name == nameof(IModificationAuditedObject.LastModifierId))
{
return true;
}
if (entityType.IsAssignableTo<ISoftDelete>()
&& propertyInfo.Name == nameof(ISoftDelete.IsDeleted))
{
return true;
}
if (entityType.IsAssignableTo<IHasDeletionTime>()
&& propertyInfo.Name == nameof(IHasDeletionTime.DeletionTime))
{
return true;
}
if (entityType.IsAssignableTo<IDeletionAuditedObject>()
&& propertyInfo.Name == nameof(IDeletionAuditedObject.DeleterId))
{
return true;
}
return false;
}
private object GetNotNull(object obj1, object obj2)
{
if (obj1 != null)
return obj1;
if (obj2 != null)
return obj2;
throw new NotSupportedException();
}
private string GetEntityId(object entityAsObj)
{
if (!(entityAsObj is IEntity entity))
{
throw new AbpException($"Entities should implement the {typeof(IEntity).AssemblyQualifiedName} interface! Given entity does not implement it: {entityAsObj.GetType().AssemblyQualifiedName}");
}
var keys = entity.GetKeys();
if (keys.All(k => k == null))
{
return null;
}
return keys.JoinAsString(",");
}
protected virtual Guid? GetTenantId(object entity)
{
if (!(entity is IMultiTenant multiTenantEntity))
{
return null;
}
return multiTenantEntity.TenantId;
}
}
Hi,
I am implementing my own entity change tracking and am using MongoDB.
There is a bug, which means that the sorting is only being applied to the current page of entity change results. https://github.com/abpframework/abp/blob/f4c7def47a97c77e4fa2813f52e8ebe61598f095/modules/audit-logging/src/Volo.Abp.AuditLogging.MongoDB/Volo/Abp/AuditLogging/MongoDB/MongoAuditLogRepository.cs#L179
Paging should be performed after the sort, same as this line: https://github.com/abpframework/abp/blob/f4c7def47a97c77e4fa2813f52e8ebe61598f095/modules/audit-logging/src/Volo.Abp.AuditLogging.MongoDB/Volo/Abp/AuditLogging/MongoDB/MongoAuditLogRepository.cs#L58
Best regards,
Mike
Hi,
I have been trying to get the entity auditing working, but then I found out through the documentation that it is not supported for MongoDB. https://docs.abp.io/en/abp/latest/Audit-Logging
Is there a reason that this has not yet been implemented for MongoDB? Can you let me know if this is planned to be supported? Can you let me know how I might go about implementing this myself?
Many thanks,
Mike
Hi,
The existing OU functionality allows us to define a hierarchy of OUs and then specify the users which are within the OUs. We are then able to define the roles for an OU, which results in those roles being applied to the users within the OU.
However, we have a requirement where we need to able to assign a role to a user, which should only give them the role permissions for the specific organizational units defined by the role.
The idea would be the following.
I have looked at all the existing documentation regarding organization units and looked at many existing software solutions to discover how it works from a users perspective and I believe the above design would work well.
I am wondering if you are planning to extend the OU functionality to include this feature in the future? Or anything similar? Is there already a request of backlog item for it?
If not, are you able to make any recommendations about how we should go about implementing this functionality?
My current thinking is
I settled on the "Restricted Access" rather than "OU Specific" as I think "OU Specific" does not make it clear what its purpose is and can be confused with the existing ability to add roles to an OU. I also chose to extend permissions to be "Restricted Access" and to make a distinction between normal roles vs "Restricted Access" roles to make it clear what a user with the role will be able to do and there would be no confusion about if the user will have access to all data or only data associated with particular OUs.
Your help is greatly appreciated,
Mike
i have not yet tried this yet, but my instinct is that maybe this is a problem with the mongodb specific version. I have not made any modifications to the main application and only created custom modules, so I would have expected this to work. Which db did you use for your test? I will create a new solution and do some more investigating when i get a chance.
When I try to move an organization unit to a different parent (e.g. move Production under Central)... I can see in the preview that I am requesting to move it where I want to
and the confirmation dialog is correct. However, after completion, it has not moved. This appears to be the case when trying to move organization units to any different level, including to the root level.
Mike
This appears to now be fixed in RC5