This is a bit weird... If I then rename the role back to the original name, the permissions reappear.
When the role is renamed and the permissions are cleared, the permissions are not applied to a logged in user, but simply renaming the role back to the original means that a logged in user is then correctly assigned the permissions.
If I set the permissions of a role and then rename the role, the permissions of the role are cleared.
Hi,
It is already possible to assign members and roles to an OU, but could it also be possible (out of the box) to assign claims to an OU? Does it make sense to be able to do this? The intention being that the claims will be assigned to any members of that OU, in the same way that the roles are applied to its members? Either way, could you point me to the relevant places where I should implement this myself?
Many thanks,
Mike
When will 4.2.1 be released? We need the fix for the modals within modals.
Do you think that it could be possible to extend the abp suite / cli to be able to specify if the generated MongoDB repositories should use either IQueryable
or IAggregateFluent
?
Or maybe after reviewing the details below, newly generated MongoDB repositories should be generated to use IAggregateFluent
and not use IQueryable
?
The automatically generated Entity Framework repositories include join
statements to include navigation properties, so that the navigation properties can be used in the OrderBy calls and returned. However, the MongoDB repositories do not have this functionality and perform separate database calls to fetch the navigation properties. This means that the navigation properties cannot be used in sort or filters (this results in a bug where sorting does not work from the UI, when it appears to a user like it should).
The default generated MongoDB repositories currently internally use IQueryable to perform any filtering. However, assuming that a developer wants to enable similar functionality as the EntityFramework repositories or take advantage of the full capabilities of MongoDB, then they should use aggregation pipelines instead and work with an IAggregateFluent
pipeline.
We have found ourselves doing exactly this, and have replaced all of the GetMongoQueryable()
calls in the repositories with an GetMongoAggregated()
extension method to return an IAggregateFluent
.
public static IAggregateFluent<TEntity> GetMongoAggregated<TMongoDbContext, TEntity>(this MongoDbRepository<TMongoDbContext, TEntity> repository)
where TMongoDbContext : IExtendedMongoDbContext
where TEntity : class, IEntity
{
IAggregateFluent<TEntity> aggregate = repository.SessionHandle != null ? repository.Collection.Aggregate(repository.SessionHandle) : repository.Collection.Aggregate();
return aggregate.ApplyDataFilters(repository);
}
private static IAggregateFluent<TEntity> ApplyDataFilters<TMongoDbContext, TEntity>(this IAggregateFluent<TEntity> aggregate, MongoDbRepository<TMongoDbContext, TEntity> repository)
where TMongoDbContext : IExtendedMongoDbContext
where TEntity : class, IEntity
{
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)) && repository.DataFilter.IsEnabled<ISoftDelete>())
{
aggregate = aggregate.Match(e => ((ISoftDelete)e).IsDeleted == false);
}
if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)) && repository.DataFilter.IsEnabled<IMultiTenant>())
{
var tenantId = repository.CurrentTenant.Id;
aggregate = aggregate.Match(e => ((IMultiTenant)e).TenantId == tenantId);
}
return aggregate;
}
The main reason for us to do this was that we can now use the aggregation pipelines to easily "Include" any entities referenced by navigation properties, which can then be used within a single database call (e.g. for sorting) and/or returned within that same query (no additional database queries needed to get the navigation property entities).
Some example usages are:
public async Task<ChannelWithNavigationProperties> GetWithNavigationPropertiesAsync(Guid id, CancellationToken cancellationToken = default)
{
var channel = await this.GetMongoAggregated()
.Match(e => e.Id == id)
.Include<Channel, ProductVersion>(DbContext)
.FirstOrDefaultAsync(GetCancellationToken(cancellationToken));
return new ChannelWithNavigationProperties
{
Channel = channel,
ProductVersion = channel.GetIncluded<ProductVersion>(),
};
}
public async Task<List<ChannelWithNavigationProperties>> GetListWithNavigationPropertiesAsync(
string filterText = null,
string sorting = null,
int maxResultCount = int.MaxValue,
int skipCount = 0,
CancellationToken cancellationToken = default)
{
var query = ApplyFilter(this.GetMongoAggregated(), filterText);
var channels = await query
.Include<Channel, ProductVersion>(DbContext)
.Sort(sorting, ChannelConsts.GetDefaultSorting(false))
.Skip(skipCount).Limit(maxResultCount)
.ToListAsync(GetCancellationToken(cancellationToken));
List<ChannelWithNavigationProperties> result = new List<ChannelWithNavigationProperties>();
foreach (var s in channels)
{
result.Add(new ChannelWithNavigationProperties
{
Channel = s,
ProductVersion = s.GetIncluded<ProductVersion>()
});
}
return result;
}
In the example, a Channel has a "ProductVersionId" navigation property. Any ApplyFilter methods can also easily be changed to use the GetMongoAggregated
extension method for consistency.
We have some extension methods which make this very easy to use.
public static IAggregateFluent<TEntity> Include<TEntity, TLookupEntity>(this IAggregateFluent<TEntity> query, IExtendedMongoDbContext dbContext, string localFieldName = null)
where TEntity : class
{
string entityName = typeof(TLookupEntity).Name;
if (string.IsNullOrEmpty(localFieldName))
{
localFieldName = entityName + "Id";
}
var queryBson = query.As<BsonDocument>();
// Perform a lookup. If it exists then it will be included in an array called entityName
queryBson = queryBson.Lookup(dbContext.GetCollectionNamePublic<TLookupEntity>(), localFieldName, "_id", entityName);
// convert the single value in the array to a property instead (use the options to handle the scenario that the foreign entity does not exist)
queryBson = queryBson.Unwind(new StringFieldDefinition<BsonDocument>(entityName), new AggregateUnwindOptions<BsonDocument>() {PreserveNullAndEmptyArrays = true});
return queryBson.As<TEntity>();
}
public static TEntity GetIncluded<TEntity>(this IHasExtraProperties entity, string fieldName = null) where TEntity : class
{
var properties = entity.ExtraProperties;
if (string.IsNullOrEmpty(fieldName))
{
fieldName = typeof(TEntity).Name;
}
if (properties.TryGetValue(fieldName, out object entityObject) && entityObject is Dictionary<string, object> dictionary)
{
return BsonSerializer.Deserialize<TEntity>(dictionary.ToBsonDocument());
}
return null;
}
/// <summary>
/// Support sort of the format "myColumn asc" or "myOtherColumn desc".
/// The first letter of any sort field is automatically capitalized.
/// Can also sort on multiple columns "myColumn asd, myOtherColumn desc".
/// Can also sort by sub properties "myColumn.something desc"
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="query"></param>
/// <param name="sorting"></param>
/// <param name="defaultSorting"></param>
/// <returns></returns>
public static IAggregateFluent<TEntity> Sort<TEntity>(this IAggregateFluent<TEntity> query, string sorting, string defaultSorting)
{
sorting = string.IsNullOrWhiteSpace(sorting) ? defaultSorting : sorting;
if (string.IsNullOrEmpty(sorting))
return query;
string[] sortParts = sorting.Split(',');
List<SortDefinition<TEntity>> sortDefinitions = new List<SortDefinition<TEntity>>();
// check if a sort definition is sorting using a property of the top level entity
string topLevelEntitySortPrefix = typeof(TEntity).Name + ".";
foreach (var sortPart in sortParts)
{
var parts = sortPart.Split(" ");
if (parts.Length == 0)
continue;
string fieldName = CorrectCase(parts[0]);
if (fieldName.StartsWith(topLevelEntitySortPrefix))
{
fieldName = fieldName.Substring(topLevelEntitySortPrefix.Length);
}
if (parts.Length == 1)
{
sortDefinitions.Add(Builders<TEntity>.Sort.Ascending(fieldName));
}
if (parts.Length > 1)
{
if (PartIsDescending(parts[1]))
{
sortDefinitions.Add(Builders<TEntity>.Sort.Descending(fieldName));
}
else
{
sortDefinitions.Add(Builders<TEntity>.Sort.Ascending(fieldName));
}
}
}
if (sortDefinitions.Any())
{
query = query.Sort(Builders<TEntity>.Sort.Combine(sortDefinitions));
}
return query;
}
private static bool PartIsDescending(string part)
{
if (string.IsNullOrEmpty(part))
return false;
return part.ToLower().Contains("des");
}
public static string CorrectCase(string input)
{
var parts = input.Split('.');
return String.Join(".", parts.Select(FirstCharToUpper));
}
private static string FirstCharToUpper(string input)
{
if (string.IsNullOrEmpty(input))
throw new ArgumentNullException(nameof(input));
return input.First().ToString().ToUpper() + input.Substring(1);
}
The IExtendedMongoDbContext
simply exposes the protected GetCollectionName<T>()
method publicly as GetCollectionNamePublic<T>()
.
The Sort
method supports being able to sort on foreign entity properties and also supports sorting on multiple columns (datatables.net => shift + click).
Whatever the outcome, I hope that this code that I am sharing might be useful for other people wanting to use MongoDB.
There are also other repositories where the methods are not all marked as virtual, e.g. MongoIdentityRoleRepository
Maybe you could add a unit test to detect and ensure that all public methods of any repository are overridable?
Hi,
When we are wanting to search for a user, we are generally replacing the default module repositories with appropriate methods overriden to allow the filter text search to be case insensitive. We are changing the filter to perform a regex (MongoDB) search with the ignore case flag set (new BsonRegularExpression(Regex.Escape(filterText), "i");
).
However, when we wanted to do this for the the MongoOrganizationUnitRepository
, the ...Unadded...
methods are not marked as virtual, so we are unable to override and change their behaviour. Our only option is to replace the whole repository and then make the changes. Could you make these methods virtual please?
Is it the intention that the filters in repositories are case sensitive? Is there a better way to change the search to be case insensitive without overriding each repository?
Mike
Nevermind, the issue was that the Microsoft.Extensions.FileProviders.Embedded
was reporting as being downgraded. I had removed the package form the domain.shared and web project, but had not added it back again. After adding it again, it fixed the problem.
I have a single solution which includes a single module. I updated both using the cli to version 4.1. After the update, I resolved the documented breaking change (where repositories had to be passed by interface) in once place.
Then I hit a problem that I am unable to resolve. My module localisation resources are not loaded correctly (menu text is not subsituted) and trying to access a page gives the following error.
AbpException: Could not find the bundle file '/Pages/LicenseManagement/Accounts/index.js' from IWebContentFileProvider Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers.AbpTagHelperResourceService.ProcessAsync(ViewContext viewContext, TagHelperContext context, TagHelperOutput output, List<BundleTagHelperItem> bundleItems, string bundleName) Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers.AbpBundleTagHelperService<TTagHelper, TService>.ProcessAsync(TagHelperContext context, TagHelperOutput output) Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner.<RunAsync>g__Awaited|0_0(Task task, TagHelperExecutionContext executionContext, int i, int count) AspNetCore.Pages_LicenseManagement_Accounts_Index.<ExecuteAsync>b__37_0() in Index.cshtml + <abp-script-bundle name="@typeof(IndexModel).FullName"> Microsoft.AspNetCore.Mvc.Razor.RazorPage.RenderSectionAsyncCore(string sectionName, bool required) AspNetCore.Themes_Lepton_Layouts_Application_Default+<>c__DisplayClass28_0+<<ExecuteAsync>b__1>d.MoveNext() Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperExecutionContext.SetOutputContentAsync() AspNetCore.Themes_Lepton_Layouts_Application_Default.ExecuteAsync() Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context) Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, bool invokeViewStarts) Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderLayoutAsync(ViewContext context, ViewBufferTextWriter bodyWriter) Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context) Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, string contentType, Nullable<int> statusCode) Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, string contentType, Nullable<int> statusCode) Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeResultAsync>g__Logged|21_0(ResourceInvoker invoker, IActionResult result) Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0<TFilter, TFilterAsync>(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted) Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context) Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext<TFilter, TFilterAsync>(ref State next, ref Scope scope, ref object state, ref bool isCompleted) Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeResultFilters>g__Awaited|27_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted) Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted) Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context) Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted) Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted) Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker) Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) Volo.Abp.AspNetCore.Auditing.AbpAuditingMiddleware.InvokeAsync(HttpContext context, RequestDelegate next) Volo.Abp.AspNetCore.Auditing.AbpAuditingMiddleware.InvokeAsync(HttpContext context, RequestDelegate next) Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass6_1+<<UseMiddlewareInterface>b__1>d.MoveNext() Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext) Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider) Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult) Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) IdentityServer4.Hosting.IdentityServerMiddleware.Invoke(HttpContext context, IEndpointRouter router, IUserSession session, IEventService events, IBackChannelLogoutService backChannelLogoutService) IdentityServer4.Hosting.MutualTlsEndpointMiddleware.Invoke(HttpContext context, IAuthenticationSchemeProvider schemes) Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) IdentityServer4.Hosting.BaseUrlMiddleware.Invoke(HttpContext context) Microsoft.AspNetCore.Builder.ApplicationBuilderAbpJwtTokenMiddlewareExtension+<>c__DisplayClass0_0+<<UseJwtTokenMiddleware>b__0>d.MoveNext() Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context) Microsoft.AspNetCore.RequestLocalization.AbpRequestLocalizationMiddleware.InvokeAsync(HttpContext context, RequestDelegate next) Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass6_1+<<UseMiddlewareInterface>b__1>d.MoveNext() Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
If I choose a display language that has a different date format (e.g. en-gb), then the time-ago is calculated incorrectly within the entity change history widget view component.
The {entityChange.ChangeTime}
needs to be replaced by {entityChange.ChangeTime:o}
to ensure that it is rendered in the ISO 8601 format, regardless of which culture is being used.
Mike