diff --git a/docs/en/Data-Filtering.md b/docs/en/Data-Filtering.md index 284f74918a..4235d2f3e4 100644 --- a/docs/en/Data-Filtering.md +++ b/docs/en/Data-Filtering.md @@ -1,3 +1,222 @@ # Data Filtering -TODO \ No newline at end of file +[Volo.Abp.Data](https://www.nuget.org/packages/Volo.Abp.Data) package defines services to automatically filter data on querying from a database. + +## Pre-Defined Filters + +ABP defines some filters out of the box. + +### ISoftDelete + +Used to mark an [entity](Entities.md) as deleted instead of actually deleting it. Implement the `ISoftDelete` interface to make your entity "soft delete". + +Example: + +````csharp +using System; +using Volo.Abp; +using Volo.Abp.Domain.Entities; + +namespace Acme.BookStore +{ + public class Book : AggregateRoot, ISoftDelete + { + public string Name { get; set; } + + public bool IsDeleted { get; set; } //Defined by ISoftDelete + } +} +```` + +`ISoftDelete` defines the `IsDeleted` property. When you delete a book using [repositories](Repositories.md), ABP automatically sets `IsDeleted` to true and protects it from actual deletion (you can also manually set the `IsDeleted` property to true if you need). In addition, it **automatically filters deleted entities** when you query the database. + +> `ISoftDelete` filter is enabled by default and you can not get deleted entities from database unless you explicitly disable it. See the `IDataFilter` service below. + +### IMultiTenant + +[Multi-tenancy](Multi-Tenancy.md) is an efficient way of creating SaaS applications. Once you create a multi-tenant application, you typically want to isolate data between tenants. Implement `IMultiTenant` interface to make your entity "multi-tenant aware". + +Example: + +````csharp +using System; +using Volo.Abp; +using Volo.Abp.Domain.Entities; +using Volo.Abp.MultiTenancy; + +namespace Acme.BookStore +{ + public class Book : AggregateRoot, ISoftDelete, IMultiTenant + { + public string Name { get; set; } + + public bool IsDeleted { get; set; } //Defined by ISoftDelete + + public Guid? TenantId { get; set; } //Defined by IMultiTenant + } +} +```` + +`IMultiTenant` interface defines the `TenantId` property which is then used to automatically filter the entities for the current tenant. See the [Multi-tenancy](Multi-Tenancy.md) document for more. + +## IDataFilter Service: Enable/Disable Data Filters + +You can control the filters using `IDataFilter` service. + +Example: + +````csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Acme.BookStore +{ + public class MyBookService : ITransientDependency + { + private readonly IDataFilter _dataFilter; + private readonly IRepository _bookRepository; + + public MyBookService( + IDataFilter dataFilter, + IRepository bookRepository) + { + _dataFilter = dataFilter; + _bookRepository = bookRepository; + } + + public async Task> GetAllBooksIncludingDeletedAsync() + { + //Temporary disable the ISoftDelete filter + using (_dataFilter.Disable()) + { + return await _bookRepository.GetListAsync(); + } + } + } +} +```` + +* [Inject](Dependency-Injection.md) the `IDataFilter` service to your class. +* Use the `Disable` method within a `using` statement to create a code block where the `ISoftDelete` filter is disabled inside it (Always use it inside a `using` block to guarantee that the filter is reset to its previous state). + +`IDataFilter.Enable` method can be used to enable a filter. `Enable` and `Disable` methods can be used in a nested way to define inner scopes. + +## AbpDataFilterOptions + +`AbpDataFilterOptions` can be used to [set options](Options.md) for the data filter system. + +The example code below disables the `ISoftDelete` filter by default which will cause to include deleted entities when you query the database unless you explicitly enable the filter: + +````csharp +Configure(options => +{ + options.DefaultStates[typeof(ISoftDelete)] = new DataFilterState(isEnabled: false); +}); +```` + +> Carefully change defaults for global filters, especially if you are using a pre-built module which might be developed assuming the soft delete filter is turned on by default. But you can do it for your own defined filters safely. + +## Defining Custom Filters + +Defining and implementing a new filter highly depends on the database provider. ABP implements all pre-defined filters for all database providers. + +When you need it, start by defining an interface (like `ISoftDelete` and `IMultiTenant`) for your filter and implement it for your entities. + +Example: + +````csharp +public interface IIsActive +{ + bool IsActive { get; } +} +```` + +Such an `IIsActive` interface can be used to filter active/passive data and can be easily implemented by any [entity](Entities.md): + +````csharp +public class Book : AggregateRoot, IIsActive +{ + public string Name { get; set; } + + public bool IsActive { get; set; } //Defined by IIsActive +} +```` + +### EntityFramework Core + +ABP uses [EF Core's Global Query Filters](https://docs.microsoft.com/en-us/ef/core/querying/filters) system for the [EF Core Integration](Entity-Framework-Core.md). So, it is well integrated to EF Core and works as expected even if you directly work with `DbContext`. + +Best way to implement a custom filter is to override `CreateFilterExpression` method for your `DbContext`. Example: + +````csharp +protected bool IsActiveFilterEnabled => DataFilter?.IsEnabled() ?? false; + +protected override Expression> CreateFilterExpression() +{ + var expression = base.CreateFilterExpression(); + + if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) + { + Expression> isActiveFilter = + e => !IsActiveFilterEnabled || EF.Property(e, "IsActive"); + expression = expression == null + ? isActiveFilter + : CombineExpressions(expression, isActiveFilter); + } + + return expression; +} +```` + +* Added a `IsActiveFilterEnabled` property to check if `IIsActive` is enabled or not. It internally uses the `IDataFilter` service introduced before. +* Overrided the `CreateFilterExpression` method, checked if given entity implements the `IIsActive` interface and combines the expressions if necessary. + +### MongoDB + +ABP implements data filters directly in the [repository](Repositories.md) level for the [MongoDB Integration](MongoDB.md). So, it works only if you use the repositories properly. Otherwise, you should manually filter the data. + +Currently, the best way to implement a data filter for the MongoDB integration is to override the `AddGlobalFilters` in the repository derived from the `MongoDbRepository` class. Example: + +````csharp +public class BookRepository : MongoDbRepository +{ + protected override void AddGlobalFilters(List> filters) + { + if (DataFilter.IsEnabled()) + { + filters.Add(Builders.Filter.Eq(e => ((IIsActive)e).IsActive, true)); + } + } +} +```` + +This example implements it only for the `Book` entity. If you want to implement for all entities (those implement the `IIsActive` interface), create your own custom MongoDB repository base class and override the `AddGlobalFilters` as shown below: + +````csharp +public abstract class MyMongoRepository : MongoDbRepository + where TMongoDbContext : IAbpMongoDbContext + where TEntity : class, IEntity +{ + protected MyMongoRepository(IMongoDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + + } + + protected override void AddGlobalFilters(List> filters) + { + if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity)) + && DataFilter.IsEnabled()) + { + filters.Add(Builders.Filter.Eq(e => ((IIsActive)e).IsActive, true)); + } + } +} +```` + +> See "Set Default Repository Classes" section of the [MongoDb Integration document](MongoDB.md) to learn how to replace default repository base with your custom class. \ No newline at end of file diff --git a/docs/en/Modules/Index.md b/docs/en/Modules/Index.md index 97af3db132..fb37f7914b 100644 --- a/docs/en/Modules/Index.md +++ b/docs/en/Modules/Index.md @@ -19,7 +19,8 @@ There are some **free and open source** application modules developed and mainta * **Identity**: Used to manage roles, users and their permissions. * **Identity Server**: Integrates to IdentityServer4. * **Permission Management**: Used to persist permissions. -* **[Setting Management](Setting-Management.md)**: Used to persist and manage the [settings](../Settings.md). +* [**Setting Management**](Setting-Management.md): Used to persist +and manage the [settings](../Settings.md). * **Tenant Management**: Used to manage tenants for a [multi-tenant](../Multi-Tenancy.md) application. * **Users**: Used to abstract users, so other modules can depend on this instead of the Identity module. diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs index 4140cceeb3..dbcfb184eb 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs @@ -23,12 +23,12 @@ namespace Volo.Abp.Caching IDistributedCache cache, ICancellationTokenProvider cancellationTokenProvider, IDistributedCacheSerializer serializer, - ICurrentTenant currentTenant) : base( + IDistributedCacheKeyNormalizer keyNormalizer) : base( distributedCacheOption: distributedCacheOption, cache: cache, cancellationTokenProvider: cancellationTokenProvider, serializer: serializer, - currentTenant: currentTenant) + keyNormalizer: keyNormalizer) { } @@ -54,7 +54,7 @@ namespace Volo.Abp.Caching protected IDistributedCacheSerializer Serializer { get; } - protected ICurrentTenant CurrentTenant { get; } + protected IDistributedCacheKeyNormalizer KeyNormalizer { get; } protected SemaphoreSlim SyncSemaphore { get; } @@ -67,29 +67,29 @@ namespace Volo.Abp.Caching IDistributedCache cache, ICancellationTokenProvider cancellationTokenProvider, IDistributedCacheSerializer serializer, - ICurrentTenant currentTenant) + IDistributedCacheKeyNormalizer keyNormalizer) { _distributedCacheOption = distributedCacheOption.Value; Cache = cache; CancellationTokenProvider = cancellationTokenProvider; Logger = NullLogger>.Instance; Serializer = serializer; - CurrentTenant = currentTenant; + KeyNormalizer = keyNormalizer; SyncSemaphore = new SemaphoreSlim(1, 1); SetDefaultOptions(); } + protected virtual string NormalizeKey(TCacheKey key) { - var normalizedKey = "c:" + CacheName + ",k:" + _distributedCacheOption.KeyPrefix + key.ToString(); - - if (!IgnoreMultiTenancy && CurrentTenant.Id.HasValue) - { - normalizedKey = "t:" + CurrentTenant.Id.Value + "," + normalizedKey; - } - - return normalizedKey; + return KeyNormalizer.NormalizeKey( + new DistributedCacheKeyNormalizeArgs( + key.ToString(), + CacheName, + IgnoreMultiTenancy + ) + ); } protected virtual DistributedCacheEntryOptions GetDefaultCacheEntryOptions() @@ -102,6 +102,7 @@ namespace Volo.Abp.Caching return options; } } + return _distributedCacheOption.GlobalCacheEntryOptions; } @@ -115,6 +116,7 @@ namespace Volo.Abp.Caching //Configure default cache entry options DefaultCacheOptions = GetDefaultCacheEntryOptions(); } + /// /// Gets a cache item with the given key. If no cache item is found for the given key then returns null. /// @@ -193,6 +195,7 @@ namespace Volo.Abp.Caching return Serializer.Deserialize(cachedBytes); } + /// /// Gets or Adds a cache item with the given key. If no cache item is found for the given key then adds a cache item /// provided by delegate and returns the provided cache item. @@ -228,6 +231,7 @@ namespace Volo.Abp.Caching return value; } + /// /// Gets or Adds a cache item with the given key. If no cache item is found for the given key then adds a cache item /// provided by delegate and returns the provided cache item. @@ -266,6 +270,7 @@ namespace Volo.Abp.Caching return value; } + /// /// Sets the cache item value for the provided key. /// @@ -300,6 +305,7 @@ namespace Volo.Abp.Caching throw; } } + /// /// Sets the cache item value for the provided key. /// @@ -338,6 +344,7 @@ namespace Volo.Abp.Caching throw; } } + /// /// Refreshes the cache value of the given key, and resets its sliding expiration timeout. /// @@ -393,6 +400,7 @@ namespace Volo.Abp.Caching throw; } } + /// /// Removes the cache item for given key from cache. /// @@ -418,6 +426,7 @@ namespace Volo.Abp.Caching throw; } } + /// /// Removes the cache item for given key from cache. /// @@ -447,7 +456,5 @@ namespace Volo.Abp.Caching throw; } } - } - } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCacheKeyNormalizeArgs.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCacheKeyNormalizeArgs.cs new file mode 100644 index 0000000000..791a1630eb --- /dev/null +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCacheKeyNormalizeArgs.cs @@ -0,0 +1,21 @@ +namespace Volo.Abp.Caching +{ + public class DistributedCacheKeyNormalizeArgs + { + public string Key { get; } + + public string CacheName { get; } + + public bool IgnoreMultiTenancy { get; } + + public DistributedCacheKeyNormalizeArgs( + string key, + string cacheName, + bool ignoreMultiTenancy) + { + Key = key; + CacheName = cacheName; + IgnoreMultiTenancy = ignoreMultiTenancy; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCacheKeyNormalizer.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCacheKeyNormalizer.cs new file mode 100644 index 0000000000..aca22705dd --- /dev/null +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCacheKeyNormalizer.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.Caching +{ + public class DistributedCacheKeyNormalizer : IDistributedCacheKeyNormalizer, ITransientDependency + { + protected ICurrentTenant CurrentTenant { get; } + + protected AbpDistributedCacheOptions DistributedCacheOptions { get; } + + public DistributedCacheKeyNormalizer( + ICurrentTenant currentTenant, + IOptions distributedCacheOptions) + { + CurrentTenant = currentTenant; + DistributedCacheOptions = distributedCacheOptions.Value; + } + + public virtual string NormalizeKey(DistributedCacheKeyNormalizeArgs args) + { + var normalizedKey = $"c:{args.CacheName},k:{DistributedCacheOptions.KeyPrefix}{args.Key}"; + + if (!args.IgnoreMultiTenancy && CurrentTenant.Id.HasValue) + { + normalizedKey = $"t:{CurrentTenant.Id.Value},{normalizedKey}"; + } + + return normalizedKey; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCacheKeyNormalizer.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCacheKeyNormalizer.cs new file mode 100644 index 0000000000..5d4499a325 --- /dev/null +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCacheKeyNormalizer.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Caching +{ + public interface IDistributedCacheKeyNormalizer + { + string NormalizeKey(DistributedCacheKeyNormalizeArgs args); + } +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs index 4d4f6f797e..44c1789907 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.Cli.Args; using Volo.Abp.Cli.ProjectBuilding; using Volo.Abp.Cli.ProjectBuilding.Building; +using Volo.Abp.Cli.Utils; using Volo.Abp.DependencyInjection; namespace Volo.Abp.Cli.Commands @@ -28,7 +29,9 @@ namespace Volo.Abp.Cli.Commands public async Task ExecuteAsync(CommandLineArgs commandLineArgs) { - if (commandLineArgs.Target == null) + var projectName = NamespaceHelper.NormalizeNamespace(commandLineArgs.Target); + + if (projectName == null) { throw new CliUsageException( "Project name is missing!" + @@ -36,9 +39,9 @@ namespace Volo.Abp.Cli.Commands GetUsageInfo() ); } - + Logger.LogInformation("Creating your project..."); - Logger.LogInformation("Project name: " + commandLineArgs.Target); + Logger.LogInformation("Project name: " + projectName); var template = commandLineArgs.Options.GetOrNull(Options.Template.Short, Options.Template.Long); if (template != null) @@ -73,7 +76,7 @@ namespace Volo.Abp.Cli.Commands var outputFolder = commandLineArgs.Options.GetOrNull(Options.OutputFolder.Short, Options.OutputFolder.Long); outputFolder = Path.Combine(outputFolder != null ? Path.GetFullPath(outputFolder) : Directory.GetCurrentDirectory(), - SolutionName.Parse(commandLineArgs.Target).FullName); + SolutionName.Parse(projectName).FullName); if (!Directory.Exists(outputFolder)) { @@ -86,7 +89,7 @@ namespace Volo.Abp.Cli.Commands var result = await TemplateProjectBuilder.BuildAsync( new ProjectBuildArgs( - SolutionName.Parse(commandLineArgs.Target), + SolutionName.Parse(projectName), template, version, databaseProvider, @@ -129,7 +132,7 @@ namespace Volo.Abp.Cli.Commands } } - Logger.LogInformation($"'{commandLineArgs.Target}' has been successfully created to '{outputFolder}'"); + Logger.LogInformation($"'{projectName}' has been successfully created to '{outputFolder}'"); } public string GetUsageInfo() diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/NamespaceHelper.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/NamespaceHelper.cs new file mode 100644 index 0000000000..80e893afcf --- /dev/null +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/NamespaceHelper.cs @@ -0,0 +1,21 @@ +using System.Text.RegularExpressions; +using JetBrains.Annotations; + +namespace Volo.Abp.Cli.Utils +{ + public static class NamespaceHelper + { + public static string NormalizeNamespace([CanBeNull] string value) + { + if (string.IsNullOrEmpty(value)) + { + return value; + } + + value = value.Trim(); + value = Regex.Replace(value, @"(((?<=\.)|^)((?=\d)|\.)|[^\w\.])|(\.$)", "_"); + + return value; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json index c589a46e38..34682c9d28 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json @@ -46,6 +46,13 @@ "DatatableActionDropdownDefaultText": "Ações", "ChangePassword": "Alterar Senha", "PersonalInfo": "Perfil", - "AreYouSureYouWantToCancelEditingWarningMessage": "Há alterações não salvas." + "AreYouSureYouWantToCancelEditingWarningMessage": "Há alterações não salvas.", + "UnhandledException": "Exceção não tratada!", + "401Message": "Autenticação inválida", + "403Message": "Não autorizado", + "404Message": "Página não encontrada", + "500Message": "Erro interno do servidor", + "GoHomePage": "Voltar à página principal", + "GoBack": "Voltar" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pt-BR.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pt-BR.json index bfc1293cb5..a533acd4f2 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pt-BR.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pt-BR.json @@ -3,7 +3,7 @@ "texts": { "UserName": "Usuário", "EmailAddress": "E-mail", - "UserNameOrEmailAddress": "Utilize seu nome de usuário ou e-mail", + "UserNameOrEmailAddress": "Usuário ou e-mail", "Password": "Senha", "RememberMe": "Lembrar", "UseAnotherServiceToLogin": "Usar outro serviço para entrar", @@ -13,10 +13,12 @@ "SelfRegistrationDisabledMessage": "Não é permitido que você crie uma nova conta neste site. Contate um administrador para que ele crie uma conta para você.", "Login": "Entrar", "Cancel": "Cancelar", - "Register": "Registrar-se", - "InvalidLoginRequest": "Requisição de acesso inválida!", + "Register": "Registre-se", + "AreYouANewUser": "Você é um novo usuário?", + "AlreadyRegistered": "Já registrado?", + "InvalidLoginRequest": "Requisição de acesso inválido!", "ThereAreNoLoginSchemesConfiguredForThisClient": "Não existe um esquema de acesso para este usuário", - "LogInUsingYourProviderAccount": "Acessse utilizando sua conta {0}", + "LogInUsingYourProviderAccount": "Acesse utilizando sua conta {0}", "DisplayName:CurrentPassword": "Senha atual", "DisplayName:NewPassword": "Nova Senha", "DisplayName:NewPasswordConfirm": "Confirmação de senha", diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs index b408828af0..c1d0f8f28b 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs @@ -127,8 +127,16 @@ namespace Volo.Abp.Identity private async Task UpdateUserByInput(IdentityUser user, IdentityUserCreateOrUpdateDtoBase input) { - (await _userManager.SetEmailAsync(user, input.Email)).CheckErrors(); - (await _userManager.SetPhoneNumberAsync(user, input.PhoneNumber)).CheckErrors(); + if (!string.Equals(user.Email, input.Email, StringComparison.InvariantCultureIgnoreCase)) + { + (await _userManager.SetEmailAsync(user, input.Email)).CheckErrors(); + } + + if (!string.Equals(user.PhoneNumber, input.PhoneNumber, StringComparison.InvariantCultureIgnoreCase)) + { + (await _userManager.SetPhoneNumberAsync(user, input.PhoneNumber)).CheckErrors(); + } + (await _userManager.SetTwoFactorEnabledAsync(user, input.TwoFactorEnabled)).CheckErrors(); (await _userManager.SetLockoutEnabledAsync(user, input.LockoutEnabled)).CheckErrors(); diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.html b/npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.html index a5460b782b..3046313407 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.html +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.html @@ -158,7 +158,7 @@ -