diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index a07b35c66b..776b61b168 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -103,7 +103,7 @@ "MasterModules": "Master Modules", "OrganizationName": "Organization name", "CreationDate": "Creation date", - "LicenseStart": "License start date", + "LicenseStartDate": "License start date", "LicenseEndDate": "License end date", "OrganizationNamePlaceholder": "Organization name...", "TotalQuestionCountPlaceholder": "Total question count...", diff --git a/build/build-all-release.ps1 b/build/build-all-release.ps1 index 1c86351379..f54912d4f5 100644 --- a/build/build-all-release.ps1 +++ b/build/build-all-release.ps1 @@ -1,4 +1,4 @@ -. ".\common.ps1" +. ".\common.ps1" -f # Build all solutions diff --git a/build/build-all.ps1 b/build/build-all.ps1 index ffaffa67a3..16f8bf037c 100644 --- a/build/build-all.ps1 +++ b/build/build-all.ps1 @@ -1,6 +1,10 @@ -. ".\common.ps1" +$full = $args[0] -# Build all solutions +. ".\common.ps1" $full + +# Build all solutions + +Write-Host $solutionPaths foreach ($solutionPath in $solutionPaths) { $solutionAbsPath = (Join-Path $rootFolder $solutionPath) diff --git a/build/common.ps1 b/build/common.ps1 index c75e77178c..18019a154e 100644 --- a/build/common.ps1 +++ b/build/common.ps1 @@ -1,26 +1,38 @@ -# COMMON PATHS +$full = $args[0] + +# COMMON PATHS $rootFolder = (Get-Item -Path "./" -Verbose).FullName -# List of solutions +# List of solutions used only in development mode +$solutionPaths = @( + "../framework", + "../modules/users", + "../modules/permission-management", + "../modules/setting-management", + "../modules/feature-management", + "../modules/identity", + "../modules/identityserver", + "../modules/tenant-management", + "../modules/audit-logging", + "../modules/background-jobs", + "../modules/account" + ) -$solutionPaths = ( - "../framework", - "../modules/users", - "../modules/permission-management", - "../modules/setting-management", - "../modules/feature-management", - "../modules/identity", - "../modules/identityserver", - "../modules/tenant-management", - "../modules/account", - "../modules/docs", - "../modules/blogging", - "../modules/audit-logging", - "../modules/background-jobs", - "../modules/client-simulation", - "../modules/virtual-file-explorer", - "../templates/module/aspnet-core", - "../templates/app/aspnet-core", - "../abp_io/AbpIoLocalization" -) \ No newline at end of file +if ($full -eq "-f") +{ + # List of additional solutions required for full build + $solutionPaths += ( + "../modules/client-simulation", + "../modules/virtual-file-explorer", + "../modules/docs", + "../modules/blogging", + "../templates/module/aspnet-core", + "../templates/app/aspnet-core", + "../abp_io/AbpIoLocalization" + ) +}else{ + Write-host "" + Write-host ":::::::::::::: !!! You are in development mode !!! ::::::::::::::" -ForegroundColor red -BackgroundColor yellow + Write-host "" +} diff --git a/build/test-all.ps1 b/build/test-all.ps1 index 1465bff6c3..9a94ec3760 100644 --- a/build/test-all.ps1 +++ b/build/test-all.ps1 @@ -1,4 +1,6 @@ -. ".\common.ps1" +$full = $args[0] + +. ".\common.ps1" $full # Test all solutions diff --git a/docs/en/Application-Services.md b/docs/en/Application-Services.md index d28a7290fb..9cfec614cb 100644 --- a/docs/en/Application-Services.md +++ b/docs/en/Application-Services.md @@ -367,6 +367,84 @@ public class DistrictKey } ```` +### Authorization (for CRUD App Services) + +There are two ways of authorizing the base application service methods; + +1. You can set the policy properties (xxxPolicyName) in the constructor of your service. Example: + +```csharp +public class MyPeopleAppService : CrudAppService +{ + public MyPeopleAppService(IRepository repository) + : base(repository) + { + GetPolicyName = "..."; + GetListPolicyName = "..."; + CreatePolicyName = "..."; + UpdatePolicyName = "..."; + DeletePolicyName = "..."; + } +} +``` + +`CreatePolicyName` is checked by the `CreateAsync` method and so on... You should specify a policy (permission) name defined in your application. + +2. You can override the check methods (CheckXxxPolicyAsync) in your service. Example: + +```csharp +public class MyPeopleAppService : CrudAppService +{ + public MyPeopleAppService(IRepository repository) + : base(repository) + { + } + + protected override async Task CheckDeletePolicyAsync() + { + await AuthorizationService.CheckAsync("..."); + } +} +``` + +You can perform any logic in the `CheckDeletePolicyAsync` method. It is expected to throw an `AbpAuthorizationException` in any unauthorized case, like `AuthorizationService.CheckAsync` already does. + +### Base Properties & Methods + +CRUD application service base class provides many useful base methods that **you can override** to customize it based on your requirements. + +#### CRUD Methods + +These are the essential CRUD methods. You can override any of them to completely customize the operation. Here, the definitions of the methods: + +````csharp +Task GetAsync(TKey id); +Task> GetListAsync(TGetListInput input); +Task CreateAsync(TCreateInput input); +Task UpdateAsync(TKey id, TUpdateInput input); +Task DeleteAsync(TKey id); +```` + +#### Querying + +These methods are low level methods those can be control how to query entities from the database. + +* `CreateFilteredQuery` can be overridden to create an `IQueryable` that is filtered by the given input. If your `TGetListInput` class contains any filter, it is proper to override this method and filter the query. It returns the (unfiltered) repository (which is already `IQueryable`) by default. +* `ApplyPaging` is used to make paging on the query. If your `TGetListInput` already implements `IPagedResultRequest`, you don't need to override this since the ABP Framework automatically understands it and performs the paging. +* `ApplySorting` is used to sort (order by...) the query. If your `TGetListInput` already implements the `ISortedResultRequest`, ABP Framework automatically sorts the query. If not, it fallbacks to the `ApplyDefaultSorting` which tries to sort by creating time, if your entity implements the standard `IHasCreationTime` interface. +* `GetEntityByIdAsync` is used to get an entity by id, which calls `Repository.GetAsync(id)` by default. +* `DeleteByIdAsync` is used to delete an entity by id, which calls `Repository.DeleteAsync(id)` by default. + +#### Object to Object Mapping + +These methods are used to convert Entities to DTOs and vice verse. They uses the [IObjectMapper](Object-To-Object-Mapping.md) by default. + +* `MapToGetOutputDtoAsync` is used to map the entity to the DTO returned from the `GetAsync`, `CreateAsync` and `UpdateAsync` methods. Alternatively, you can override the `MapToGetOutputDto` if you don't need to perform any async operation. +* `MapToGetListOutputDtosAsync` is used to map a list of entities to a list of DTOs returned from the `GetListAsync` method. It uses the `MapToGetListOutputDtoAsync` to map each entity in the list. You can override one of them based on your case. Alternatively, you can override the `MapToGetListOutputDto` if you don't need to perform any async operation. +* `MapToEntityAsync` method has two overloads; + * `MapToEntityAsync(TCreateInput)` is used to create an entity from `TCreateInput`. + * `MapToEntityAsync(TUpdateInput, TEntity)` is used to update an existing entity from `TUpdateInput`. + ## Lifetime Lifetime of application services are [transient](Dependency-Injection.md) and they are automatically registered to the dependency injection system. diff --git a/docs/en/Blob-Storing-Aliyun.md b/docs/en/Blob-Storing-Aliyun.md index 6621e2dbd2..602d666787 100644 --- a/docs/en/Blob-Storing-Aliyun.md +++ b/docs/en/Blob-Storing-Aliyun.md @@ -45,17 +45,19 @@ Configure(options => * **AccessKeyId** ([NotNull]string): AccessKey is the key to access the Alibaba Cloud API. It has full permissions for the account. Please keep it safe! Recommend to follow [Alibaba Cloud security best practicess](https://help.aliyun.com/document_detail/102600.html),Use RAM sub-user AccessKey to call API. * **AccessKeySecret** ([NotNull]string): Same as above. -* **Endpoint** ([NotNull]string): Endpoint is the external domain name of OSS. See the [document](https://help.aliyun.com/document_detail/31837.html) for details. +* **Endpoint** ([NotNull]string): Endpoint is the external domain name of OSS. See the [document](https://help.aliyun.com/document_detail/31837.html) for details. +* **UseSecurityTokenService** (bool): Use [STS temporary credentials](https://help.aliyun.com/document_detail/100624.html) to access OSS services,default: `false`. * **RegionId** (string): Access address of STS service. See the [document](https://help.aliyun.com/document_detail/66053.html) for details. * **RoleArn** ([NotNull]string): STS required role ARN. See the [document](https://help.aliyun.com/document_detail/100624.html) for details. * **RoleSessionName** ([NotNull]string): Used to identify the temporary access credentials, it is recommended to use different application users to distinguish. * **Policy** (string): Additional permission restrictions. See the [document](https://help.aliyun.com/document_detail/100680.html) for details. -* **DurationSeconds** (int): Validity period(s) of a temporary access certificate,minimum is 900 and the maximum is 3600. **note**: Using subaccounts operated OSS,if the value is 0. +* **DurationSeconds** (int): Validity period(s) of a temporary access certificate,minimum is 900 and the maximum is 3600. * **ContainerName** (string): You can specify the container name in Aliyun. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aliyun has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://help.aliyun.com/knowledge_detail/39668.html): * Container names must start or end with a letter or number, and can contain only letters, numbers, and the dash (-) character. * Container names Must start and end with lowercase letters and numbers. * Container names must be from **3** through **63** characters long. * **CreateContainerIfNotExists** (bool): Default value is `false`, If a container does not exist in Aliyun, `AliyunBlobProvider` will try to create it. +* **TemporaryCredentialsCacheKey** (bool): The cache key of STS credentials. ## Aliyun Blob Name Calculator @@ -70,4 +72,4 @@ Aliyun Blob Provider organizes BLOB name and implements some conventions. The fu * `AliyunBlobProvider` is the main service that implements the Aliyun BLOB storage provider, if you want to override/replace it via [dependency injection](Dependency-Injection.md) (don't replace `IBlobProvider` interface, but replace `AliyunBlobProvider` class). * `IAliyunBlobNameCalculator` is used to calculate the full BLOB name (that is explained above). It is implemented by the `DefaultAliyunBlobNameCalculator` by default. -* `IOssClientFactory` is used create OSS client. It is implemented by the `DefaultOssClientFactory` by default. You can override/replace it,if you want customize. \ No newline at end of file +* `IOssClientFactory` is used create OSS client. It is implemented by the `DefaultOssClientFactory` by default. You can override/replace it,if you want customize. diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index c71787f598..3008db5c41 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -4,7 +4,7 @@ ABP Framework provides a simple, yet efficient text template system. Text templating is used to dynamically render contents based on a template and a model (a data object): -***TEMPLATE + MODEL ==render==> RENDERED CONTENT*** +Template + Model =renderer=> Rendered Content It is very similar to an ASP.NET Core Razor View (or Page): @@ -454,4 +454,4 @@ Return `null` if your source can not find the content, so `ITemplateContentProvi * [The source code of the sample application](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) developed and referred through this document. * [Localization system](Localization.md). -* [Virtual File System](Virtual-File-System.md). \ No newline at end of file +* [Virtual File System](Virtual-File-System.md). diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md index 5a38e821bc..11b608b0eb 100644 --- a/docs/en/Tutorials/Part-1.md +++ b/docs/en/Tutorials/Part-1.md @@ -27,7 +27,7 @@ end In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: -* **{{DB_Text}}** as the ORM provider. +* **{{DB_Text}}** as the ORM provider. * **{{UI_Value}}** as the UI Framework. This tutorial is organized as the following parts; @@ -37,6 +37,11 @@ This tutorial is organized as the following parts; - [Part 3: Creating, updating and deleting books](Part-3.md) - [Part 4: Integration tests](Part-4.md) - [Part 5: Authorization](Part-5.md) +- [Part 6: Authors: Domain layer](Part-6.md) +- [Part 7: Authors: Database Integration](Part-7.md) +- [Part 8: Authors: Application Layer](Part-8.md) +- [Part 9: Authors: User Interface](Part-9.md) +- [Part 10: Book to Author Relation](Part-10.md) ### Download the Source Code diff --git a/docs/en/Tutorials/Part-10.md b/docs/en/Tutorials/Part-10.md new file mode 100644 index 0000000000..fbecf39b86 --- /dev/null +++ b/docs/en/Tutorials/Part-10.md @@ -0,0 +1,920 @@ +# Web Application Development Tutorial - Part 10: Book to Author Relation +````json +//[doc-params] +{ + "UI": ["MVC","NG"], + "DB": ["EF","Mongo"] +} +```` +{{ +if UI == "MVC" + UI_Text="mvc" +else if UI == "NG" + UI_Text="angular" +else + UI_Text="?" +end +if DB == "EF" + DB_Text="Entity Framework Core" +else if DB == "Mongo" + DB_Text="MongoDB" +else + DB_Text="?" +end +}} + +## About This Tutorial + +In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: + +* **{{DB_Text}}** as the ORM provider. +* **{{UI_Value}}** as the UI Framework. + +This tutorial is organized as the following parts; + +- [Part 1: Creating the server side](Part-1.md) +- [Part 2: The book list page](Part-2.md) +- [Part 3: Creating, updating and deleting books](Part-3.md) +- [Part 4: Integration tests](Part-4.md) +- [Part 5: Authorization](Part-5.md) +- [Part 6: Authors: Domain layer](Part-6.md) +- [Part 7: Authors: Database Integration](Part-7.md) +- [Part 8: Authors: Application Layer](Part-8.md) +- [Part 9: Authors: User Interface](Part-9.md) +- **Part 10: Book to Author Relation (this part)** + +### Download the Source Code + +This tutorials has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: + +* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) + +## Introduction + +We have created `Book` and `Author` functionalities for the book store application. However, currently there is no relation between these entities. + +In this tutorial, we will establish a **1 to N** relation between the `Book` and the `Author`. + +## Add Relation to The Book Entity + +Open the `Books/Book.cs` in the `Acme.BookStore.Domain` project and add the following property to the `Book` entity: + +````csharp +public Guid AuthorId { get; set; } +```` + +{{if DB=="EF"}} + +> In this tutorial, we preferred to not add a **navigation property** to the `Author` entity (like `public Author Author { get; set; }`). This is due to follow the DDD best practices (rule: refer to other aggregates only by id). However, you can add such a navigation property and configure it for the EF Core. In this way, you don't need to write join queries while getting books with their entities (just like we will done below) which makes your application code simpler. + +{{end}} + +## Database & Data Migration + +Added a new, required `AuthorId` property to the `Book` entity. But, what about the existing books on the database? They currently don't have `AuthorId`s and this will be a problem when we try to run the application. + +This is a typical migration problem and the decision depends on your case; + +* If you haven't published your application to the production yet, you can just delete existing books in the database, or you can even delete the entire database in your development environment. +* You can do it programmatically on data migration or seed phase. +* You can manually handle it on the database. + +We prefer to delete the database {{if DB=="EF"}}(run the `Drop-Database` in the *Package Manager Console*){{end}} since this is just an example project and data loss is not important. Since this topic is not related to the ABP Framework, we don't go deeper for all the scenarios. + +{{if DB=="EF"}} + +### Update the EF Core Mapping + +Open the `BookStoreDbContextModelCreatingExtensions` class under the `EntityFrameworkCore` folder of the `Acme.BookStore.EntityFrameworkCore` project and change the `builder.Entity` part as shown below: + +````csharp +builder.Entity(b => +{ + b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema); + b.ConfigureByConvention(); //auto configure for the base class props + b.Property(x => x.Name).IsRequired().HasMaxLength(128); + + // ADD THE MAPPING FOR THE RELATION + b.HasOne().WithMany().HasForeignKey(x => x.AuthorId).IsRequired(); +}); +```` + +### Add New EF Core Migration + +Run the following command in the Package Manager Console (of the Visual Studio) to add a new database migration: + +````bash +Add-Migration "Added_AuthorId_To_Book" +```` + +This should create a new migration class with the following code in its `Up` method: + +````csharp +migrationBuilder.AddColumn( + name: "AuthorId", + table: "AppBooks", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + +migrationBuilder.CreateIndex( + name: "IX_AppBooks_AuthorId", + table: "AppBooks", + column: "AuthorId"); + +migrationBuilder.AddForeignKey( + name: "FK_AppBooks_AppAuthors_AuthorId", + table: "AppBooks", + column: "AuthorId", + principalTable: "AppAuthors", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); +```` + +* Adds an `AuthorId` field to the `AppBooks` table. +* Creates an index on the `AuthorId` field. +* Declares the foreign key to the `AppAuthors` table. + +{{end}} + +## Change the Data Seeder + +Since the `AuthorId` is a required property of the `Book` entity, current data seeder code can not work. Open the `BookStoreDataSeederContributor` in the `Acme.BookStore.Domain` project and change as the following: + +````csharp +using System; +using System.Threading.Tasks; +using Acme.BookStore.Authors; +using Acme.BookStore.Books; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Acme.BookStore +{ + public class BookStoreDataSeederContributor + : IDataSeedContributor, ITransientDependency + { + private readonly IRepository _bookRepository; + private readonly IAuthorRepository _authorRepository; + private readonly AuthorManager _authorManager; + + public BookStoreDataSeederContributor( + IRepository bookRepository, + IAuthorRepository authorRepository, + AuthorManager authorManager) + { + _bookRepository = bookRepository; + _authorRepository = authorRepository; + _authorManager = authorManager; + } + + public async Task SeedAsync(DataSeedContext context) + { + if (await _bookRepository.GetCountAsync() > 0) + { + return; + } + + var orwell = await _authorRepository.InsertAsync( + await _authorManager.CreateAsync( + "George Orwell", + new DateTime(1903, 06, 25), + "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." + ) + ); + + var douglas = await _authorRepository.InsertAsync( + await _authorManager.CreateAsync( + "Douglas Adams", + new DateTime(1952, 03, 11), + "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." + ) + ); + + await _bookRepository.InsertAsync( + new Book + { + AuthorId = orwell.Id, // SET THE AUTHOR + Name = "1984", + Type = BookType.Dystopia, + PublishDate = new DateTime(1949, 6, 8), + Price = 19.84f + }, + autoSave: true + ); + + await _bookRepository.InsertAsync( + new Book + { + AuthorId = douglas.Id, // SET THE AUTHOR + Name = "The Hitchhiker's Guide to the Galaxy", + Type = BookType.ScienceFiction, + PublishDate = new DateTime(1995, 9, 27), + Price = 42.0f + }, + autoSave: true + ); + } + } +} +```` + +The only change is that we set the `AuthorId` properties of the `Book` entities. + +{{if DB=="EF"}} + +You can now run the `.DbMigrator` console application to **migrate** the **database schema** and **seed** the initial data. + +{{else if DB=="Mongo"}} + +You can now run the `.DbMigrator` console application to **seed** the initial data. + +{{end}} + +## Application Layer + +We will change the `BookAppService` to support the Author relation. + +### Data Transfer Objects + +Let's begin from the DTOs. + +#### BookDto + +Open the `BookDto` class in the `Books` folder of the `Acme.BookStore.Application.Contracts` project and add the following properties: + +```csharp +public Guid AuthorId { get; set; } +public string AuthorName { get; set; } +``` + +The final `BookDto` class should be following: + +```csharp +using System; +using Volo.Abp.Application.Dtos; + +namespace Acme.BookStore.Books +{ + public class BookDto : AuditedEntityDto + { + public Guid AuthorId { get; set; } + + public string AuthorName { get; set; } + + public string Name { get; set; } + + public BookType Type { get; set; } + + public DateTime PublishDate { get; set; } + + public float Price { get; set; } + } +} +``` + +#### CreateUpdateBookDto + +Open the `CreateUpdateBookDto` class in the `Books` folder of the `Acme.BookStore.Application.Contracts` project and add an `AuthorId` property as shown: + +````csharp +public Guid AuthorId { get; set; } +```` + +#### AuthorLookupDto + +Create a new class, `AuthorLookupDto`, inside the `Books` folder of the `Acme.BookStore.Application.Contracts` project: + +````csharp +using System; +using Volo.Abp.Application.Dtos; + +namespace Acme.BookStore.Books +{ + public class AuthorLookupDto : EntityDto + { + public string Name { get; set; } + } +} +```` + +This will be used in a new method will be added to the `IBookAppService`. + +### IBookAppService + +Open the `IBookAppService` interface in the `Books` folder of the `Acme.BookStore.Application.Contracts` project and add a new method, named `GetAuthorLookupAsync`, as shown below: + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Acme.BookStore.Books +{ + public interface IBookAppService : + ICrudAppService< //Defines CRUD methods + BookDto, //Used to show books + Guid, //Primary key of the book entity + PagedAndSortedResultRequestDto, //Used for paging/sorting + CreateUpdateBookDto> //Used to create/update a book + { + // ADD the NEW METHOD + Task> GetAuthorLookupAsync(); + } +} +```` + +This new method will be used from the UI to get a list of authors and fill a dropdown list to select the author of a book. + +### BookAppService + +Open the `BookAppService` interface in the `Books` folder of the `Acme.BookStore.Application` project and replace the file content with the following code: + +{{if DB=="EF"}} + +```csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Acme.BookStore.Authors; +using Acme.BookStore.Permissions; +using Microsoft.AspNetCore.Authorization; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Repositories; + +namespace Acme.BookStore.Books +{ + [Authorize(BookStorePermissions.Books.Default)] + public class BookAppService : + CrudAppService< + Book, //The Book entity + BookDto, //Used to show books + Guid, //Primary key of the book entity + PagedAndSortedResultRequestDto, //Used for paging/sorting + CreateUpdateBookDto>, //Used to create/update a book + IBookAppService //implement the IBookAppService + { + private readonly IAuthorRepository _authorRepository; + + public BookAppService( + IRepository repository, + IAuthorRepository authorRepository) + : base(repository) + { + _authorRepository = authorRepository; + GetPolicyName = BookStorePermissions.Books.Default; + GetListPolicyName = BookStorePermissions.Books.Default; + CreatePolicyName = BookStorePermissions.Books.Create; + UpdatePolicyName = BookStorePermissions.Books.Edit; + DeletePolicyName = BookStorePermissions.Books.Create; + } + + public override async Task GetAsync(Guid id) + { + //Prepare a query to join books and authors + var query = from book in Repository + join author in _authorRepository on book.AuthorId equals author.Id + where book.Id == id + select new { book, author }; + + //Execute the query and get the book with author + var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query); + if (queryResult == null) + { + throw new EntityNotFoundException(typeof(Book), id); + } + + var bookDto = ObjectMapper.Map(queryResult.book); + bookDto.AuthorName = queryResult.author.Name; + return bookDto; + } + + public override async Task> + GetListAsync(PagedAndSortedResultRequestDto input) + { + //Prepare a query to join books and authors + var query = from book in Repository + join author in _authorRepository on book.AuthorId equals author.Id + orderby input.Sorting + select new {book, author}; + + query = query + .Skip(input.SkipCount) + .Take(input.MaxResultCount); + + //Execute the query and get a list + var queryResult = await AsyncExecuter.ToListAsync(query); + + //Convert the query result to a list of BookDto objects + var bookDtos = queryResult.Select(x => + { + var bookDto = ObjectMapper.Map(x.book); + bookDto.AuthorName = x.author.Name; + return bookDto; + }).ToList(); + + //Get the total count with another query + var totalCount = await Repository.GetCountAsync(); + + return new PagedResultDto( + totalCount, + bookDtos + ); + } + + public async Task> GetAuthorLookupAsync() + { + var authors = await _authorRepository.GetListAsync(); + + return new ListResultDto( + ObjectMapper.Map, List>(authors) + ); + } + } +} +``` + +Let's see the changes we've done: + +* Added `[Authorize(BookStorePermissions.Books.Default)]` to authorize the methods we've newly added/overrode (remember, authorize attribute is valid for all the methods of the class when it is declared for a class). +* Injected `IAuthorRepository` to query from the authors. +* Overrode the `GetAsync` method of the base `CrudAppService`, which returns a single `BookDto` object with the given `id`. + * Used a simple LINQ expression to join books and authors and query them together for the given book id. + * Used `AsyncExecuter.FirstOrDefaultAsync(...)` to execute the query and get a result. `AsyncExecuter` was previously used in the `AuthorAppService`. Check the [repository documentation](../Repositories.md) to understand why we've used it. + * Throws an `EntityNotFoundException` which results an `HTTP 404` (not found) result if requested book was not present in the database. + * Finally, created a `BookDto` object using the `ObjectMapper`, then assigning the `AuthorName` manually. +* Overrode the `GetListAsync` method of the base `CrudAppService`, which returns a list of books. The logic is similar to the previous method, so you can easily understand the code. +* Created a new method: `GetAuthorLookupAsync`. This simple gets all the authors. The UI uses this method to fill a dropdown list and select and author while creating/editing books. + +{{else if DB=="Mongo"}} + +```csharp +using System; +using System.Collections.Generic; +using System.Linq.Dynamic.Core; +using System.Linq; +using System.Threading.Tasks; +using Acme.BookStore.Authors; +using Acme.BookStore.Permissions; +using Microsoft.AspNetCore.Authorization; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace Acme.BookStore.Books +{ + [Authorize(BookStorePermissions.Books.Default)] + public class BookAppService : + CrudAppService< + Book, //The Book entity + BookDto, //Used to show books + Guid, //Primary key of the book entity + PagedAndSortedResultRequestDto, //Used for paging/sorting + CreateUpdateBookDto>, //Used to create/update a book + IBookAppService //implement the IBookAppService + { + private readonly IAuthorRepository _authorRepository; + + public BookAppService( + IRepository repository, + IAuthorRepository authorRepository) + : base(repository) + { + _authorRepository = authorRepository; + GetPolicyName = BookStorePermissions.Books.Default; + GetListPolicyName = BookStorePermissions.Books.Default; + CreatePolicyName = BookStorePermissions.Books.Create; + UpdatePolicyName = BookStorePermissions.Books.Edit; + DeletePolicyName = BookStorePermissions.Books.Create; + } + + public override async Task GetAsync(Guid id) + { + var book = await Repository.GetAsync(id); + var bookDto = ObjectMapper.Map(book); + + var author = await _authorRepository.GetAsync(book.AuthorId); + bookDto.AuthorName = author.Name; + + return bookDto; + } + + public override async Task> + GetListAsync(PagedAndSortedResultRequestDto input) + { + //Set a default sorting, if not provided + if (input.Sorting.IsNullOrWhiteSpace()) + { + input.Sorting = nameof(Book.Name); + } + + //Get the books + var books = await AsyncExecuter.ToListAsync( + Repository + .OrderBy(input.Sorting) + .Skip(input.SkipCount) + .Take(input.MaxResultCount) + ); + + //Convert to DTOs + var bookDtos = ObjectMapper.Map, List>(books); + + //Get a lookup dictionary for the related authors + var authorDictionary = await GetAuthorDictionaryAsync(books); + + //Set AuthorName for the DTOs + bookDtos.ForEach(bookDto => bookDto.AuthorName = + authorDictionary[bookDto.AuthorId].Name); + + //Get the total count with another query (required for the paging) + var totalCount = await Repository.GetCountAsync(); + + return new PagedResultDto( + totalCount, + bookDtos + ); + } + + public async Task> GetAuthorLookupAsync() + { + var authors = await _authorRepository.GetListAsync(); + + return new ListResultDto( + ObjectMapper.Map, List>(authors) + ); + } + + private async Task> + GetAuthorDictionaryAsync(List books) + { + var authorIds = books + .Select(b => b.AuthorId) + .Distinct() + .ToArray(); + + var authors = await AsyncExecuter.ToListAsync( + _authorRepository.Where(a => authorIds.Contains(a.Id)) + ); + + return authors.ToDictionary(x => x.Id, x => x); + } + } +} +``` + +Let's see the changes we've done: + +* Added `[Authorize(BookStorePermissions.Books.Default)]` to authorize the methods we've newly added/overrode (remember, authorize attribute is valid for all the methods of the class when it is declared for a class). +* Injected `IAuthorRepository` to query from the authors. +* Overrode the `GetAsync` method of the base `CrudAppService`, which returns a single `BookDto` object with the given `id`. +* Overrode the `GetListAsync` method of the base `CrudAppService`, which returns a list of books. This code separately queries the authors from database and sets the name of the authors in the application code. Instead, you could create a custom repository method and perform a join query or take the power of the MongoDB API to get the books and their authors in a single query, which would be more performant. +* Created a new method: `GetAuthorLookupAsync`. This simple gets all the authors. The UI uses this method to fill a dropdown list and select and author while creating/editing books. + +{{end}} + +### Object to Object Mapping Configuration + +Introduced the `AuthorLookupDto` class and used object mapping inside the `GetAuthorLookupAsync` method. So, we need to add a new mapping definition inside the `BookStoreApplicationAutoMapperProfile.cs` file of the `Acme.BookStore.Application` project: + +````csharp +CreateMap(); +```` + +## Unit Tests + +Some of the unit tests will fail since we made some changed on the `AuthorAppService`. Open the `BookAppService_Tests` in the `Books` folder of the `Acme.BookStore.Application.Tests` project and change the content as the following: + +```csharp +using System; +using System.Linq; +using System.Threading.Tasks; +using Acme.BookStore.Authors; +using Shouldly; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Validation; +using Xunit; + +namespace Acme.BookStore.Books +{ {{if DB=="Mongo"}} + [Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} + public class BookAppService_Tests : BookStoreApplicationTestBase + { + private readonly IBookAppService _bookAppService; + private readonly IAuthorAppService _authorAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + _authorAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Books() + { + //Act + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + + //Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "1984" && + b.AuthorName == "George Orwell"); + } + + [Fact] + public async Task Should_Create_A_Valid_Book() + { + var authors = await _authorAppService.GetListAsync(new GetAuthorListDto()); + var firstAuthor = authors.Items.First(); + + //Act + var result = await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + AuthorId = firstAuthor.Id, + Name = "New test book 42", + Price = 10, + PublishDate = System.DateTime.Now, + Type = BookType.ScienceFiction + } + ); + + //Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New test book 42"); + } + + [Fact] + public async Task Should_Not_Create_A_Book_Without_Name() + { + var exception = await Assert.ThrowsAsync(async () => + { + await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "", + Price = 10, + PublishDate = DateTime.Now, + Type = BookType.ScienceFiction + } + ); + }); + + exception.ValidationErrors + .ShouldContain(err => err.MemberNames.Any(m => m == "Name")); + } + } +} +``` + +* Changed the assertion condition in the `Should_Get_List_Of_Books` from `b => b.Name == "1984"` to `b => b.Name == "1984" && b.AuthorName == "George Orwell"` to check if the author name was filled. +* Changed the `Should_Create_A_Valid_Book` method to set the `AuthorId` while creating a new book, since it is required anymore. + +## The User Interface + +{{if UI=="MVC"}} + +### The Book List + +Book list page change is trivial. Open the `Pages/Books/Index.js` in the `Acme.BookStore.Web` project and add the following column definition between the `name` and `type` columns: + +````js +... +{ + title: l('Name'), + data: "name" +}, + +// ADDED the NEW AUTHOR NAME COLUMN +{ + title: l('Author'), + data: "authorName" +}, + +{ + title: l('Type'), + data: "type", + render: function (data) { + return l('Enum:BookType:' + data); + } +}, +... +```` + +When you run the application, you can see the *Author* column on the table: + +![bookstore-added-author-to-book-list](images/bookstore-added-author-to-book-list.png) + +### Create Modal + +Open the `Pages/Books/CreateModal.cshtml.cs` in the `Acme.BookStore.Web` project and change the file content as shown below: + +```csharp +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Acme.BookStore.Books; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +namespace Acme.BookStore.Web.Pages.Books +{ + public class CreateModalModel : BookStorePageModel + { + [BindProperty] + public CreateBookViewModel Book { get; set; } + + public List Authors { get; set; } + + private readonly IBookAppService _bookAppService; + + public CreateModalModel( + IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnGetAsync() + { + Book = new CreateBookViewModel(); + + var authorLookup = await _bookAppService.GetAuthorLookupAsync(); + Authors = authorLookup.Items + .Select(x => new SelectListItem(x.Name, x.Id.ToString())) + .ToList(); + } + + public async Task OnPostAsync() + { + await _bookAppService.CreateAsync( + ObjectMapper.Map(Book) + ); + return NoContent(); + } + + public class CreateBookViewModel + { + [SelectItems(nameof(Authors))] + [DisplayName("Author")] + public Guid AuthorId { get; set; } + + [Required] + [StringLength(128)] + public string Name { get; set; } + + [Required] + public BookType Type { get; set; } = BookType.Undefined; + + [Required] + [DataType(DataType.Date)] + public DateTime PublishDate { get; set; } = DateTime.Now; + + [Required] + public float Price { get; set; } + } + } +} +``` + +* Changed type of the `Book` property from `CreateUpdateBookDto` to the new `CreateBookViewModel` class defined in this file. The main motivation of this change to customize the model class based on the User Interface (UI) requirements. We didn't want to use UI-related `[SelectItems(nameof(Authors))]` and `[DisplayName("Author")]` attributes inside the `CreateUpdateBookDto` class. +* Added `Authors` property that is filled inside the `OnGetAsync` method using the `IBookAppService.GetAuthorLookupAsync` method defined before. +* Changed the `OnPostAsync` method to map `CreateBookViewModel` object to a `CreateUpdateBookDto` object since `IBookAppService.CreateAsync` expects a parameter of this type. + +### Edit Modal + +Open the `Pages/Books/EditModal.cshtml.cs` in the `Acme.BookStore.Web` project and change the file content as shown below: + +```csharp +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Acme.BookStore.Books; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +namespace Acme.BookStore.Web.Pages.Books +{ + public class EditModalModel : BookStorePageModel + { + [BindProperty] + public EditBookViewModel Book { get; set; } + + public List Authors { get; set; } + + private readonly IBookAppService _bookAppService; + + public EditModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnGetAsync(Guid id) + { + var bookDto = await _bookAppService.GetAsync(id); + Book = ObjectMapper.Map(bookDto); + + var authorLookup = await _bookAppService.GetAuthorLookupAsync(); + Authors = authorLookup.Items + .Select(x => new SelectListItem(x.Name, x.Id.ToString())) + .ToList(); + } + + public async Task OnPostAsync() + { + await _bookAppService.UpdateAsync( + Book.Id, + ObjectMapper.Map(Book) + ); + + return NoContent(); + } + + public class EditBookViewModel + { + [HiddenInput] + public Guid Id { get; set; } + + [SelectItems(nameof(Authors))] + [DisplayName("Author")] + public Guid AuthorId { get; set; } + + [Required] + [StringLength(128)] + public string Name { get; set; } + + [Required] + public BookType Type { get; set; } = BookType.Undefined; + + [Required] + [DataType(DataType.Date)] + public DateTime PublishDate { get; set; } = DateTime.Now; + + [Required] + public float Price { get; set; } + } + } +} +``` + +* Changed type of the `Book` property from `CreateUpdateBookDto` to the new `EditBookViewModel` class defined in this file, just like done before for the create modal above. +* Moved the `Id` property inside the new `EditBookViewModel` class. +* Added `Authors` property that is filled inside the `OnGetAsync` method using the `IBookAppService.GetAuthorLookupAsync` method. +* Changed the `OnPostAsync` method to map `EditBookViewModel` object to a `CreateUpdateBookDto` object since `IBookAppService.UpdateAsync` expects a parameter of this type. + +These changes require a small change in the `EditModal.cshtml`. Remove the `` tag since we no longer need to it (since moved it to the `EditBookViewModel`). The final content of the `EditModal.cshtml` should be following: + +````html +@page +@using Acme.BookStore.Localization +@using Acme.BookStore.Web.Pages.Books +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model EditModalModel +@inject IStringLocalizer L +@{ + Layout = null; +} + + + + + + + + + +```` + +### Object to Object Mapping Configuration + +The changes above requires to define some object to object mappings. Open the `BookStoreWebAutoMapperProfile.cs` in the `Acme.BookStore.Web` project and add the following mapping definitions inside the constructor: + +```csharp +CreateMap(); +CreateMap(); +CreateMap(); +``` + +You can run the application and try to create a new book or update an existing book. You will see a drop down list on the create/update form to select the author of the book: + +![bookstore-added-authors-to-modals](images/bookstore-added-authors-to-modals.png) + +{{else if UI=="NG"}} + +***Angular UI is being prepared...*** + +{{end}} \ No newline at end of file diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index 690394264c..1f3b6c84d1 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/docs/en/Tutorials/Part-2.md @@ -37,6 +37,11 @@ This tutorial is organized as the following parts; - [Part 3: Creating, updating and deleting books](Part-3.md) - [Part 4: Integration tests](Part-4.md) - [Part 5: Authorization](Part-5.md) +- [Part 6: Authors: Domain layer](Part-6.md) +- [Part 7: Authors: Database Integration](Part-7.md) +- [Part 8: Authors: Application Layer](Part-8.md) +- [Part 9: Authors: User Interface](Part-9.md) +- [Part 10: Book to Author Relation](Part-10.md) ### Download the Source Code @@ -51,7 +56,7 @@ This tutorials has multiple versions based on your **UI** and **Database** prefe It's common to call the HTTP API endpoints via AJAX from the **JavaScript** side. You can use `$.ajax` or another tool to call the endpoints. However, ABP offers a better way. -ABP **dynamically** creates **[JavaScript Proxies](../UI/AspNetCore/)** for all API endpoints. So, you can use any **endpoint** just like calling a **JavaScript function**. +ABP **dynamically** creates **[JavaScript Proxies](../UI/AspNetCore/Dynamic-JavaScript-Proxies.md)** for all API endpoints. So, you can use any **endpoint** just like calling a **JavaScript function**. ### Testing in the Developer Console @@ -457,12 +462,9 @@ For more information, see the [RoutesService document](https://docs.abp.io/en/ab Run the following command in the `angular` folder: ```bash -abp generate-proxy --apiUrl https://localhost:XXXXX +abp generate-proxy ``` -* XXXXX should be replaced with the backend port of your application. -* If you don't specify the `--apiUrl` parameter, it will try to get the URL from the `src/environments/environment.ts` file. - The generated files looks like below: ![Generated files](./images/generated-proxies-2.png) @@ -474,7 +476,7 @@ Open the `/src/app/book/book.component.ts` file and replace the content as below ```js import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { BookDto, BookType } from './models'; +import { BookDto } from './models'; import { BookService } from './services'; @Component({ @@ -486,8 +488,6 @@ import { BookService } from './services'; export class BookComponent implements OnInit { book = { items: [], totalCount: 0 } as PagedResultDto; - booksType = BookType; - constructor(public readonly list: ListService, private bookService: BookService) {} ngOnInit() { diff --git a/docs/en/Tutorials/Part-3.md b/docs/en/Tutorials/Part-3.md index 3b31ec1c88..a0e839186c 100644 --- a/docs/en/Tutorials/Part-3.md +++ b/docs/en/Tutorials/Part-3.md @@ -32,11 +32,16 @@ In this tutorial series, you will build an ABP based web application named `Acme This tutorial is organized as the following parts; -- [Part 1: Creating the project and book list page](Part-1.md) +- [Part 1: Creating the server side](Part-1.md) - [Part 2: The book list page](Part-2.md) - **Part 3: Creating, updating and deleting books (this part)** - [Part 4: Integration tests](Part-4.md) - [Part 5: Authorization](Part-5.md) +- [Part 6: Authors: Domain layer](Part-6.md) +- [Part 7: Authors: Database Integration](Part-7.md) +- [Part 8: Authors: Application Layer](Part-8.md) +- [Part 9: Authors: User Interface](Part-9.md) +- [Part 10: Book to Author Relation](Part-10.md) ### Download the Source Code @@ -643,7 +648,7 @@ Open `/src/app/book/book.component.ts` and replace the content as below: ```js import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { BookDto, BookType } from './models'; +import { BookDto } from './models'; import { BookService } from './services'; @Component({ @@ -655,8 +660,6 @@ import { BookService } from './services'; export class BookComponent implements OnInit { book = { items: [], totalCount: 0 } as PagedResultDto; - booksType = BookType; - isModalOpen = false; // add this line constructor(public readonly list: ListService, private bookService: BookService) {} @@ -738,7 +741,7 @@ Open `/src/app/book/book.component.ts` and replace the content as below: ```js import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { BookDto, BookType } from './models'; +import { BookDto, BookType } from './models'; // add BookType import { BookService } from './services'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // add this @@ -751,13 +754,13 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // add this export class BookComponent implements OnInit { book = { items: [], totalCount: 0 } as PagedResultDto; - booksType = BookType; - form: FormGroup; // add this line - // add bookTypes as a list of enum members - bookTypes = Object.keys(BookType).filter( - (bookType) => typeof this.booksType[bookType] === 'number' + bookType = BookType; // add this line + + // add bookTypes as a list of BookType enum members + bookTypes = Object.keys(this.bookType).filter( + (key) => typeof this.bookType[key] === 'number' ); isModalOpen = false; @@ -808,7 +811,8 @@ export class BookComponent implements OnInit { * Imported `FormGroup`, `FormBuilder` and `Validators` from `@angular/forms`. * Added `form: FormGroup` property. -* Add `bookTypes` as a list of `BookType` enum members. +* Added `bookType` property so that you can reach `BookType` enum members from template. +* Added `bookTypes` property as a list of `BookType` enum members. That will be used in form options. * Injected `FormBuilder` into the constructor. [FormBuilder](https://angular.io/api/forms/FormBuilder) provides convenient methods for generating form controls. It reduces the amount of boilerplate needed to build complex forms. * Added `buildForm` method to the end of the file and executed the `buildForm()` in the `createBook` method. * Added `save` method. @@ -832,7 +836,7 @@ Open `/src/app/book/book.component.html` and replace ` Type * @@ -917,13 +921,12 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap export class BookComponent implements OnInit { book = { items: [], totalCount: 0 } as PagedResultDto; - booksType = BookType; - form: FormGroup; - // <== added bookTypes array ==> - bookTypes = Object.keys(BookType).filter( - (bookType) => typeof this.booksType[bookType] === 'number' + bookType = BookType; + + bookTypes = Object.keys(this.bookType).filter( + (key) => typeof this.bookType[key] === 'number' ); isModalOpen = false; @@ -998,14 +1001,14 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap export class BookComponent implements OnInit { book = { items: [], totalCount: 0 } as PagedResultDto; - booksType = BookType; + selectedBook = new BookDto(); // declare selectedBook form: FormGroup; - selectedBook = new BookDto(); // declare selectedBook + bookType = BookType; - bookTypes = Object.keys(BookType).filter( - (bookType) => typeof this.booksType[bookType] === 'number' + bookTypes = Object.keys(this.bookType).filter( + (key) => typeof this.bookType[key] === 'number' ); isModalOpen = false; @@ -1182,4 +1185,4 @@ Clicking the "Delete" action calls the `delete` method which then shows a confir ## The Next Part -See the [next part](part-4.md) of this tutorial. +See the [next part](Part-4.md) of this tutorial. diff --git a/docs/en/Tutorials/Part-4.md b/docs/en/Tutorials/Part-4.md index 5e87d18653..acbce65c3e 100644 --- a/docs/en/Tutorials/Part-4.md +++ b/docs/en/Tutorials/Part-4.md @@ -32,11 +32,16 @@ In this tutorial series, you will build an ABP based web application named `Acme This tutorial is organized as the following parts; -- [Part 1: Creating the project and book list page](Part-1.md) +- [Part 1: Creating the server side](Part-1.md) - [Part 2: The book list page](Part-2.md) - [Part 3: Creating, updating and deleting books](Part-3.md) - **Part 4: Integration tests (this part)** - [Part 5: Authorization](Part-5.md) +- [Part 6: Authors: Domain layer](Part-6.md) +- [Part 7: Authors: Database Integration](Part-7.md) +- [Part 8: Authors: Application Layer](Part-8.md) +- [Part 9: Authors: User Interface](Part-9.md) +- [Part 10: Book to Author Relation](Part-10.md) ### Download the Source Code @@ -247,4 +252,4 @@ Congratulations, the **green icons** indicates that the tests have been successf ## The Next Part -See the [next part](part-5.md) of this tutorial. \ No newline at end of file +See the [next part](Part-5.md) of this tutorial. \ No newline at end of file diff --git a/docs/en/Tutorials/Part-5.md b/docs/en/Tutorials/Part-5.md index 537f79ca79..5674cf83d6 100644 --- a/docs/en/Tutorials/Part-5.md +++ b/docs/en/Tutorials/Part-5.md @@ -32,11 +32,16 @@ In this tutorial series, you will build an ABP based web application named `Acme This tutorial is organized as the following parts; -- [Part 1: Creating the project and book list page](Part-1.md) +- [Part 1: Creating the server side](Part-1.md) - [Part 2: The book list page](Part-2.md) - [Part 3: Creating, updating and deleting books](Part-3.md) - [Part 4: Integration tests](Part-4.md) - **Part 5: Authorization (this part)** +- [Part 6: Authors: Domain layer](Part-6.md) +- [Part 7: Authors: Database Integration](Part-7.md) +- [Part 8: Authors: Application Layer](Part-8.md) +- [Part 9: Authors: User Interface](Part-9.md) +- [Part 10: Book to Author Relation](Part-10.md) ### Download the Source Code @@ -399,3 +404,6 @@ Open the `/src/app/book/book.component.html` file and replace the edit and delet {{end}} +## The Next Part + +See the [next part](Part-6.md) of this tutorial. \ No newline at end of file diff --git a/docs/en/Tutorials/Part-6.md b/docs/en/Tutorials/Part-6.md new file mode 100644 index 0000000000..1bb07007b6 --- /dev/null +++ b/docs/en/Tutorials/Part-6.md @@ -0,0 +1,289 @@ +# Web Application Development Tutorial - Part 6: Authors: Domain Layer +````json +//[doc-params] +{ + "UI": ["MVC","NG"], + "DB": ["EF","Mongo"] +} +```` +{{ +if UI == "MVC" + UI_Text="mvc" +else if UI == "NG" + UI_Text="angular" +else + UI_Text="?" +end +if DB == "EF" + DB_Text="Entity Framework Core" +else if DB == "Mongo" + DB_Text="MongoDB" +else + DB_Text="?" +end +}} + +## About This Tutorial + +In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: + +* **{{DB_Text}}** as the ORM provider. +* **{{UI_Value}}** as the UI Framework. + +This tutorial is organized as the following parts; + +- [Part 1: Creating the server side](Part-1.md) +- [Part 2: The book list page](Part-2.md) +- [Part 3: Creating, updating and deleting books](Part-3.md) +- [Part 4: Integration tests](Part-4.md) +- [Part 5: Authorization](Part-5.md) +- **Part 6: Authors: Domain layer (this part)** +- [Part 7: Authors: Database Integration](Part-7.md) +- [Part 8: Authors: Application Layer](Part-8.md) +- [Part 9: Authors: User Interface](Part-9.md) +- [Part 10: Book to Author Relation](Part-10.md) + +### Download the Source Code + +This tutorials has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: + +* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) + +## Introduction + +In the previous parts, we've used the ABP infrastructure to easily build some services; + +* Used the [CrudAppService](../Application-Services.md) base class instead of manually developing an application service for standard create, read, update and delete operations. +* Used [generic repositories](../Repositories.md) to completely automate the database layer. + +For the "Authors" part; + +* We will **do some of the things manually** to show how you can do it in case of need. +* We will implement some **Domain Driven Design (DDD) best practices**. + +> **The development will be done layer by layer to concentrate on an individual layer in one time. In a real project, you will develop your application feature by feature (vertical) as done in the previous parts. In this way, you will experience both approaches.** + +## The Author Entity + +Create an `Authors` folder (namespace) in the `Acme.BookStore.Domain` project and add an `Author` class inside it: + +````csharp +using System; +using JetBrains.Annotations; +using Volo.Abp; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Acme.BookStore.Authors +{ + public class Author : FullAuditedAggregateRoot + { + public string Name { get; private set; } + public DateTime BirthDate { get; set; } + public string ShortBio { get; set; } + + private Author() + { + /* This constructor is for deserialization / ORM purpose */ + } + + internal Author( + Guid id, + [NotNull] string name, + DateTime birthDate, + [CanBeNull] string shortBio = null) + : base(id) + { + SetName(name); + BirthDate = birthDate; + ShortBio = shortBio; + } + + internal Author ChangeName([NotNull] string name) + { + SetName(name); + return this; + } + + private void SetName([NotNull] string name) + { + Name = Check.NotNullOrWhiteSpace( + name, + nameof(name), + maxLength: AuthorConsts.MaxNameLength + ); + } + } +} +```` + +* Inherited from `FullAuditedAggregateRoot` which makes the entity [soft delete](../Data-Filtering.md) (that means when you delete it, it is not deleted in the database, but just marked as deleted) with all the [auditing](../Entities.md) properties. +* `private set` for the `Name` property restricts to set this property from out of this class. There are two ways of setting the name (in both cases, we validate the name): + * In the constructor, while creating a new author. + * Using the `ChangeName` method to update the name later. +* The `constructor` and the `ChangeName` method is `internal` to force to use these methods only in the domain layer, using the `AuthorManager` that will be explained later. +* `Check` class is an ABP Framework utility class to help you while checking method arguments (it throws `ArgumentException` on an invalid case). + +`AuthorConsts` is a simple class that is located under the `Authors` namespace (folder) of the `Acme.BookStore.Domain.Shared` project: + +````csharp +namespace Acme.BookStore.Authors +{ + public static class AuthorConsts + { + public const int MaxNameLength = 64; + } +} +```` + +Created this class inside the `Acme.BookStore.Domain.Shared` project since we will re-use it on the [Data Transfer Objects](../Data-Transfer-Objects.md) (DTOs) later. + +## AuthorManager: The Domain Service + +`Author` constructor and `ChangeName` method is `internal`, so they can be usable only in the domain layer. Create an `AuthorManager` class in the `Authors` folder (namespace) of the `Acme.BookStore.Domain` project: + +````csharp +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Volo.Abp; +using Volo.Abp.Domain.Services; + +namespace Acme.BookStore.Authors +{ + public class AuthorManager : DomainService + { + private readonly IAuthorRepository _authorRepository; + + public AuthorManager(IAuthorRepository authorRepository) + { + _authorRepository = authorRepository; + } + + public async Task CreateAsync( + [NotNull] string name, + DateTime birthDate, + [CanBeNull] string shortBio = null) + { + Check.NotNullOrWhiteSpace(name, nameof(name)); + + var existingAuthor = await _authorRepository.FindByNameAsync(name); + if (existingAuthor != null) + { + throw new AuthorAlreadyExistsException(name); + } + + return new Author( + GuidGenerator.Create(), + name, + birthDate, + shortBio + ); + } + + public async Task ChangeNameAsync( + [NotNull] Author author, + [NotNull] string newName) + { + Check.NotNull(author, nameof(author)); + Check.NotNullOrWhiteSpace(newName, nameof(newName)); + + var existingAuthor = await _authorRepository.FindByNameAsync(newName); + if (existingAuthor != null && existingAuthor.Id != author.Id) + { + throw new AuthorAlreadyExistsException(newName); + } + + author.ChangeName(newName); + } + } +} +```` + +* `AuthorManager` forces to create an author and change name of an author in a controlled way. The application layer (will be introduced later) will use these methods. + +> **DDD tip**: Do not introduce domain service methods unless they are really needed and perform some core business rules. For this case, we needed to this service to be able to force the unique name constraint. + +Both methods checks if there is already an author with the given name and throws a special business exception, `AuthorAlreadyExistsException`, defined in the `Acme.BookStore.Domain` project as shown below: + +````csharp +using Volo.Abp; + +namespace Acme.BookStore.Authors +{ + public class AuthorAlreadyExistsException : BusinessException + { + public AuthorAlreadyExistsException(string name) + : base(BookStoreDomainErrorCodes.AuthorAlreadyExists) + { + WithData("name", name); + } + } +} +```` + +`BusinessException` is a special exception type. It is a good practice to throw domain related exceptions when needed. It is automatically handled by the ABP Framework and can be easily localized. `WithData(...)` method is used to provide additional data to the exception object that will later be used on the localization message or for some other purpose. + +Open the `BookStoreDomainErrorCodes` in the `Acme.BookStore.Domain.Shared` project and change as shown below: + +````csharp +namespace Acme.BookStore +{ + public static class BookStoreDomainErrorCodes + { + public const string AuthorAlreadyExists = "BookStore:00001"; + } +} +```` + +This is a unique string represents the error code thrown by your application and can be handled by client applications. For users, you probably want to localize it. Open the `Localization/BookStore/en.json` inside the `Acme.BookStore.Domain.Shared` project and add the following entry: + +````json +"BookStore:00001": "There is already an author with the same name: {name}" +```` + +Whenever you throw an `AuthorAlreadyExistsException`, the end use will see a nice error message on the UI. + +## IAuthorRepository + +`AuthorManager` injects the `IAuthorRepository`, so we need to define it. Create this new interface in the `Authors` folder (namespace) of the `Acme.BookStore.Domain` project: + +````csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Acme.BookStore.Authors +{ + public interface IAuthorRepository : IRepository + { + Task FindByNameAsync(string name); + + Task> GetListAsync( + int skipCount, + int maxResultCount, + string sorting, + string filter = null + ); + } +} +```` + +* `IAuthorRepository` extends the standard `IRepository` interface, so all the standard [repository](../Repositories.md) methods will also be available for the `IAuthorRepository`. +* `FindByNameAsync` was used in the `AuthorManager` to query an author by name. +* `GetListAsync` will be used in the application layer to get a listed, sorted and filtered list of authors to show on the UI. + +We will implement this repository in the next part. + +> Both of these methods might **seem unnecessary** since the standard repositories already `IQueryable` and you can directly use them instead of defining such custom methods. You're right and do it like in a real application. However, for this **"learning" tutorial**, it is useful to explain how to create custom repository methods when you really need it. + +## Conclusion + +This part covered the domain layer of the authors functionality of the book store application. The main files created/updated in this part was highlighted in the picture below: + +![bookstore-author-domain-layer](images/bookstore-author-domain-layer.png) + +## The Next Part + +See the [next part](Part-7.md) of this tutorial. \ No newline at end of file diff --git a/docs/en/Tutorials/Part-7.md b/docs/en/Tutorials/Part-7.md new file mode 100644 index 0000000000..81fd07f1bf --- /dev/null +++ b/docs/en/Tutorials/Part-7.md @@ -0,0 +1,236 @@ +# Web Application Development Tutorial - Part 7: Authors: Database Integration +````json +//[doc-params] +{ + "UI": ["MVC","NG"], + "DB": ["EF","Mongo"] +} +```` +{{ +if UI == "MVC" + UI_Text="mvc" +else if UI == "NG" + UI_Text="angular" +else + UI_Text="?" +end +if DB == "EF" + DB_Text="Entity Framework Core" +else if DB == "Mongo" + DB_Text="MongoDB" +else + DB_Text="?" +end +}} + +## About This Tutorial + +In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: + +* **{{DB_Text}}** as the ORM provider. +* **{{UI_Value}}** as the UI Framework. + +This tutorial is organized as the following parts; + +- [Part 1: Creating the server side](Part-1.md) +- [Part 2: The book list page](Part-2.md) +- [Part 3: Creating, updating and deleting books](Part-3.md) +- [Part 4: Integration tests](Part-4.md) +- [Part 5: Authorization](Part-5.md) +- [Part 6: Authors: Domain layer](Part-6.md) +- **Part 7: Authors: Database Integration (this part)** +- [Part 8: Authors: Application Layer](Part-8.md) +- [Part 9: Authors: User Interface](Part-9.md) +- [Part 10: Book to Author Relation](Part-10.md) + +### Download the Source Code + +This tutorials has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: + +* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) + +## Introduction + +This part explains how to configure the database integration for the `Author` entity introduced in the previous part. + +{{if DB=="EF"}} + +## DB Context + +Open the `BookStoreDbContext` in the `Acme.BookStore.EntityFrameworkCore` project and add the following `DbSet` property: + +````csharp +public DbSet Authors { get; set; } +```` + +Then open the `BookStoreDbContextModelCreatingExtensions` class in the same project and add the following lines to the end of the `ConfigureBookStore` method: + +````csharp +builder.Entity(b => +{ + b.ToTable(BookStoreConsts.DbTablePrefix + "Authors", + BookStoreConsts.DbSchema); + + b.ConfigureByConvention(); + + b.Property(x => x.Name) + .IsRequired() + .HasMaxLength(AuthorConsts.MaxNameLength); + + b.HasIndex(x => x.Name); +}); +```` + +This is just like done for the `Book` entity before, so no need to explain again. + +## Create a new Database Migration + +Open the **Package Manager Console** on Visual Studio and ensure that the **Default project** is `Acme.BookStore.EntityFrameworkCore.DbMigrations` in the Package Manager Console, as shown on the picture below. Also, set the `Acme.BookStore.Web` as the startup project (right click it on the solution explorer and click to "Set as Startup Project"). + +Run the following command to create a new database migration: + +![bookstore-add-migration-authors](images/bookstore-add-migration-authors.png) + +This will create a new migration class. Then run the `Update-Database` command to create the table on the database. + +> See the [Microsoft's documentation](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) for more about the EF Core database migrations. + +{{else if DB=="Mongo"}} + +## DB Context + +Open the `BookStoreMongoDbContext` in the `MongoDb` folder of the `Acme.BookStore.MongoDB` project and add the following property to the class: + +````csharp +public IMongoCollection Authors => Collection(); +```` + +{{end}} + +## Implementing the IAuthorRepository + +{{if DB=="EF"}} + +Create a new class, named `EfCoreAuthorRepository` inside the `Acme.BookStore.EntityFrameworkCore` project (in the `Authors` folder) and paste the following code: + +````csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading.Tasks; +using Acme.BookStore.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Acme.BookStore.Authors +{ + public class EfCoreAuthorRepository + : EfCoreRepository, + IAuthorRepository + { + public EfCoreAuthorRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync(string name) + { + return await DbSet.FirstOrDefaultAsync(author => author.Name == name); + } + + public async Task> GetListAsync( + int skipCount, + int maxResultCount, + string sorting, + string filter = null) + { + return await DbSet + .WhereIf( + !filter.IsNullOrWhiteSpace(), + author => author.Name.Contains(filter) + ) + .OrderBy(sorting) + .Skip(skipCount) + .Take(maxResultCount) + .ToListAsync(); + } + } +} +```` + +* Inherited from the `EfCoreAuthorRepository`, so it inherits the standard repository method implementations. +* `WhereIf` is a shortcut extension method of the ABP Framework. It adds the `Where` condition only if the first condition meets (it filters by name, only if the filter was provided). You could do the same yourself, but these type of shortcut methods makes our life easier. +* `sorting` can be a string like `Name`, `Name ASC` or `Name DESC`. It is possible by using the [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) NuGet package. + +> See the [EF Core Integration document](../Entity-Framework-Core.md) for more information on the EF Core based repositories. + +{{else if DB=="Mongo"}} + +Create a new class, named `MongoDbAuthorRepository` inside the `Acme.BookStore.MongoDB` project (in the `Authors` folder) and paste the following code: + +```csharp +using System; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Collections.Generic; +using System.Threading.Tasks; +using Acme.BookStore.MongoDB; +using MongoDB.Driver; +using MongoDB.Driver.Linq; +using Volo.Abp.Domain.Repositories.MongoDB; +using Volo.Abp.MongoDB; + +namespace Acme.BookStore.Authors +{ + public class MongoDbAuthorRepository + : MongoDbRepository, + IAuthorRepository + { + public MongoDbAuthorRepository( + IMongoDbContextProvider dbContextProvider + ) : base(dbContextProvider) + { + } + + public async Task FindByNameAsync(string name) + { + return await GetMongoQueryable() + .FirstOrDefaultAsync(author => author.Name == name); + } + + public async Task> GetListAsync( + int skipCount, + int maxResultCount, + string sorting, + string filter = null) + { + return await GetMongoQueryable() + .WhereIf>( + !filter.IsNullOrWhiteSpace(), + author => author.Name.Contains(filter) + ) + .OrderBy(sorting) + .As>() + .Skip(skipCount) + .Take(maxResultCount) + .ToListAsync(); + } + } +} +``` + +* Inherited from the `MongoDbAuthorRepository`, so it inherits the standard repository method implementations. +* `WhereIf` is a shortcut extension method of the ABP Framework. It adds the `Where` condition only if the first condition meets (it filters by name, only if the filter was provided). You could do the same yourself, but these type of shortcut methods makes our life easier. +* `sorting` can be a string like `Name`, `Name ASC` or `Name DESC`. It is possible by using the [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) NuGet package. + +> See the [MongoDB Integration document](../MongoDB.md) for more information on the MongoDB based repositories. + +{{end}} + +## The Next Part + +See the [next part](Part-8.md) of this tutorial. \ No newline at end of file diff --git a/docs/en/Tutorials/Part-8.md b/docs/en/Tutorials/Part-8.md new file mode 100644 index 0000000000..38e5b6013f --- /dev/null +++ b/docs/en/Tutorials/Part-8.md @@ -0,0 +1,575 @@ +# Web Application Development Tutorial - Part 8: Authors: Application Layer +````json +//[doc-params] +{ + "UI": ["MVC","NG"], + "DB": ["EF","Mongo"] +} +```` +{{ +if UI == "MVC" + UI_Text="mvc" +else if UI == "NG" + UI_Text="angular" +else + UI_Text="?" +end +if DB == "EF" + DB_Text="Entity Framework Core" +else if DB == "Mongo" + DB_Text="MongoDB" +else + DB_Text="?" +end +}} + +## About This Tutorial + +In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: + +* **{{DB_Text}}** as the ORM provider. +* **{{UI_Value}}** as the UI Framework. + +This tutorial is organized as the following parts; + +- [Part 1: Creating the server side](Part-1.md) +- [Part 2: The book list page](Part-2.md) +- [Part 3: Creating, updating and deleting books](Part-3.md) +- [Part 4: Integration tests](Part-4.md) +- [Part 5: Authorization](Part-5.md) +- [Part 6: Authors: Domain layer](Part-6.md) +- [Part 7: Authors: Database Integration](Part-7.md) +- **Part 8: Author: Application Layer (this part)** +- [Part 9: Authors: User Interface](Part-9.md) +- [Part 10: Book to Author Relation](Part-10.md) + +### Download the Source Code + +This tutorials has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: + +* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) + +## Introduction + +This part explains to create an application layer for the `Author` entity created before. + +## IAuthorAppService + +We will first create the [application service](../Application-Services.md) interface and the related [DTO](../Data-Transfer-Objects.md)s. Create a new interface, named `IAuthorAppService`, in the `Authors` namespace (folder) of the `Acme.BookStore.Application.Contracts` project: + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Acme.BookStore.Authors +{ + public interface IAuthorAppService : IApplicationService + { + Task GetAsync(Guid id); + + Task> GetListAsync(GetAuthorListDto input); + + Task CreateAsync(CreateAuthorDto input); + + Task UpdateAsync(Guid id, UpdateAuthorDto input); + + Task DeleteAsync(Guid id); + } +} +```` + +* `IApplicationService` is a conventional interface that is inherited by all the application services, so the ABP Framework can identify the service. +* Defined standard methods to perform CRUD operations on the `Author` entity. +* `PagedResultDto` is a pre-defined DTO class in the ABP Framework. It has an `Items` collection and a `TotalCount` property to return a paged result. +* Preferred to return an `AuthorDto` (for the newly created author) from the `CreateAsync` method, while it is not used by this application - just to show a different usage. + +This interface is using the DTOs defined below (create them for your project). + +### AuthorDto + +````csharp +using System; +using Volo.Abp.Application.Dtos; + +namespace Acme.BookStore.Authors +{ + public class AuthorDto : EntityDto + { + public string Name { get; set; } + + public DateTime BirthDate { get; set; } + + public string ShortBio { get; set; } + } +} +```` + +* `EntityDto` simply has an `Id` property with the given generic argument. You could create an `Id` property yourself instead of inheriting the `EntityDto`. + +### GetAuthorListDto + +````csharp +using Volo.Abp.Application.Dtos; + +namespace Acme.BookStore.Authors +{ + public class GetAuthorListDto : PagedAndSortedResultRequestDto + { + public string Filter { get; set; } + } +} +```` + +* `Filter` is used to search authors. It can be `null` (or empty string) to get all the authors. +* `PagedAndSortedResultRequestDto` has the standard paging and sorting properties: `int MaxResultCount`, `int SkipCount` and `string Sorting`. + +> ABP Framework has such base DTO classes to simplify and standardize your DTOs. See the [DTO documentation](../Data-Transfer-Objects.md) for all. + +### CreateAuthorDto + +````csharp +using System; +using System.ComponentModel.DataAnnotations; + +namespace Acme.BookStore.Authors +{ + public class CreateAuthorDto + { + [Required] + [StringLength(AuthorConsts.MaxNameLength)] + public string Name { get; set; } + + [Required] + public DateTime BirthDate { get; set; } + + public string ShortBio { get; set; } + } +} +```` + +Data annotation attributes can be used to validate the DTO. See the [validation document](../Validation.md) for details. + +### UpdateAuthorDto + +````csharp +using System; +using System.ComponentModel.DataAnnotations; + +namespace Acme.BookStore.Authors +{ + public class UpdateAuthorDto + { + [Required] + [StringLength(AuthorConsts.MaxNameLength)] + public string Name { get; set; } + + [Required] + public DateTime BirthDate { get; set; } + + public string ShortBio { get; set; } + } +} +```` + +> We could share (re-use) the same DTO among the create and the update operations. While you can do it, we prefer to create different DTOs for these operations since we see they generally be different by the time. So, code duplication is reasonable here compared to a tightly coupled design. + +## AuthorAppService + +It is time to implement the `IAuthorAppService` interface. Create a new class, named `AuthorAppService` in the `Authors` namespace (folder) of the `Acme.BookStore.Application` project: + +````csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Acme.BookStore.Permissions; +using Microsoft.AspNetCore.Authorization; +using Volo.Abp.Application.Dtos; + +namespace Acme.BookStore.Authors +{ + [Authorize(BookStorePermissions.Authors.Default)] + public class AuthorAppService : BookStoreAppService, IAuthorAppService + { + private readonly IAuthorRepository _authorRepository; + private readonly AuthorManager _authorManager; + + public AuthorAppService( + IAuthorRepository authorRepository, + AuthorManager authorManager) + { + _authorRepository = authorRepository; + _authorManager = authorManager; + } + + //...SERVICE METHODS WILL COME HERE... + } +} +```` + +* `[Authorize(BookStorePermissions.Authors.Default)]` is a declarative way to check a permission (policy) to authorize the current user. See the [authorization document](../Authorization.md) for more. `BookStorePermissions` class will be updated below, don't worry for the compile error for now. +* Derived from the `BookStoreAppService`, which is a simple base class comes with the startup template. It is derived from the standard `ApplicationService` class. +* Implemented the `IAuthorAppService` which was defined above. +* Injected the `IAuthorRepository` and `AuthorManager` to use in the service methods. + +Now, we will introduce the service methods one by one. Copy the explained method into the `AuthorAppService` class. + +### GetAsync + +````csharp +public async Task GetAsync(Guid id) +{ + var author = await _authorRepository.GetAsync(id); + return ObjectMapper.Map(author); +} +```` + +This method simply gets the `Author` entity by its `Id`, converts to the `AuthorDto` using the [object to object mapper](../Object-To-Object-Mapping.md). This requires to configure the AutoMapper, which will be explained later. + +### GetListAsync + +````csharp +public async Task> GetListAsync(GetAuthorListDto input) +{ + if (input.Sorting.IsNullOrWhiteSpace()) + { + input.Sorting = nameof(Author.Name); + } + + var authors = await _authorRepository.GetListAsync( + input.SkipCount, + input.MaxResultCount, + input.Sorting, + input.Filter + ); + + var totalCount = await AsyncExecuter.CountAsync( + _authorRepository.WhereIf( + !input.Filter.IsNullOrWhiteSpace(), + author => author.Name.Contains(input.Filter) + ) + ); + + return new PagedResultDto( + totalCount, + ObjectMapper.Map, List>(authors) + ); +} +```` + +* Default sorting is "by author name" which is done in the beginning of the method in case of it wasn't sent by the client. +* Used the `IAuthorRepository.GetListAsync` to get a paged, sorted and filtered list of authors from the database. We had implemented it in the previous part of this tutorial. Again, it actually was not needed to create such a method since we could directly query over the repository, but wanted to demonstrate how to create custom repository methods. +* Directly queried from the `AuthorRepository` while getting the count of the authors. We preferred to use the `AsyncExecuter` service which allows us to perform async queries without depending on the EF Core. However, you could depend on the EF Core package and directly use the `_authorRepository.WhereIf(...).ToListAsync()` method. See the [repository document](../Repositories.md) to read the alternative approaches and the discussion. +* Finally, returning a paged result by mapping the list of `Author`s to a list of `AuthorDto`s. + +### CreateAsync + +````csharp +[Authorize(BookStorePermissions.Authors.Create)] +public async Task CreateAsync(CreateAuthorDto input) +{ + var author = await _authorManager.CreateAsync( + input.Name, + input.BirthDate, + input.ShortBio + ); + + await _authorRepository.InsertAsync(author); + + return ObjectMapper.Map(author); +} +```` + +* `CreateAsync` requires the `BookStorePermissions.Authors.Create` permission (in addition to the `BookStorePermissions.Authors.Default` declared for the `AuthorAppService` class). +* Used the `AuthorManeger` (domain service) to create a new author. +* Used the `IAuthorRepository.InsertAsync` to insert the new author to the database. +* Used the `ObjectMapper` to return an `AuthorDto` representing the newly created author. + +> **DDD tip**: Some developers may find useful to insert the new entity inside the `_authorManager.CreateAsync`. We think it is a better design to leave it to the application layer since it better knows when to insert it to the database (maybe it requires additional works on the entity before insert, which would require to an additional update if we perform the insert in the domain service). However, it is completely up to you. + +### UpdateAsync + +````csharp +[Authorize(BookStorePermissions.Authors.Edit)] +public async Task UpdateAsync(Guid id, UpdateAuthorDto input) +{ + var author = await _authorRepository.GetAsync(id); + + if (author.Name != input.Name) + { + await _authorManager.ChangeNameAsync(author, input.Name); + } + + author.BirthDate = input.BirthDate; + author.ShortBio = input.ShortBio; + + await _authorRepository.UpdateAsync(author); +} +```` + +* `UpdateAsync` requires the additional `BookStorePermissions.Authors.Edit` permission. +* Used the `IAuthorRepository.GetAsync` to get the author entity from the database. `GetAsync` throws `EntityNotFoundException` if there is no author with the given id, which results a `404` HTTP status code in a web application. It is a good practice to always bring the entity on an update operation. +* Used the `AuthorManager.ChangeNameAsync` (domain service method) to change the author name if it was requested to change by the client. +* Directly updated the `BirthDate` and `ShortBio` since there is not any business rule to change these properties, they accept any value. +* Finally, called the `IAuthorRepository.UpdateAsync` method to update the entity on the database. + +{{if DB == "EF"}} + +> **EF Core tip**: Entity Framework Core has a **change tracking** system and **automatically saves** any change to an entity at the end of the unit of work (You can simply think that the ABP Framework automatically calls `SaveChanges` at the end of the method). So, it will work as expected even if you don't call the `_authorRepository.UpdateAsync(...)` in the end of the method. If you don't consider to change the EF Core later, you can just remove this line. + +{{end}} + +### DeleteAsync + +````csharp +[Authorize(BookStorePermissions.Authors.Delete)] +public async Task DeleteAsync(Guid id) +{ + await _authorRepository.DeleteAsync(id); +} +```` + +* `DeleteAsync` requires the additional `BookStorePermissions.Authors.Delete` permission. +* It simply uses the `DeleteAsync` method of the repository. + +## Permission Definitions + +You can't compile the code since it is expecting some constants declared in the `BookStorePermissions` class. + +Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project and change the content as shown below: + +````csharp +namespace Acme.BookStore.Permissions +{ + public static class BookStorePermissions + { + public const string GroupName = "BookStore"; + + public static class Books + { + public const string Default = GroupName + ".Books"; + public const string Create = Default + ".Create"; + public const string Edit = Default + ".Edit"; + public const string Delete = Default + ".Delete"; + } + + // *** ADDED a NEW NESTED CLASS *** + public static class Authors + { + public const string Default = GroupName + ".Authors"; + public const string Create = Default + ".Create"; + public const string Edit = Default + ".Edit"; + public const string Delete = Default + ".Delete"; + } + } +} +```` + +Then open the `BookStorePermissionDefinitionProvider` in the same project and add the following lines at the end of the `Define` method: + +````csharp +var authorsPermission = bookStoreGroup.AddPermission( + BookStorePermissions.Authors.Default, L("Permission:Authors")); + +authorsPermission.AddChild( + BookStorePermissions.Authors.Create, L("Permission:Authors.Create")); + +authorsPermission.AddChild( + BookStorePermissions.Authors.Edit, L("Permission:Authors.Edit")); + +authorsPermission.AddChild( + BookStorePermissions.Authors.Delete, L("Permission:Authors.Delete")); +```` + +Finally, add the following entries to the `Localization/BookStore/en.json` inside the `Acme.BookStore.Domain.Shared` project, to localize the permission names: + +````csharp +"Permission:Authors": "Author Management", +"Permission:Authors.Create": "Creating new authors", +"Permission:Authors.Edit": "Editing the authors", +"Permission:Authors.Delete": "Deleting the authors" +```` + +## Object to Object Mapping + +`AuthorAppService` is using the `ObjectMapper` to convert the `Author` objects to `AuthorDto` objects. So, we need to define this mapping in the AutoMapper configuration. + +Open the `BookStoreApplicationAutoMapperProfile` class inside the `Acme.BookStore.Application` project and add the following line to the constructor: + +````csharp +CreateMap(); +```` + +## Data Seeder + +As just done for the books before, it would be good to have some initial author entities in the database. This will be good while running the application first time, but also it is very useful for the automated tests. + +Open the `BookStoreDataSeederContributor` in the `Acme.BookStore.Domain` project and change the file content with the code below: + +````csharp +using System; +using System.Threading.Tasks; +using Acme.BookStore.Authors; +using Acme.BookStore.Books; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; + +namespace Acme.BookStore +{ + public class BookStoreDataSeederContributor + : IDataSeedContributor, ITransientDependency + { + private readonly IRepository _bookRepository; + private readonly IAuthorRepository _authorRepository; + private readonly AuthorManager _authorManager; + + public BookStoreDataSeederContributor( + IRepository bookRepository, + IAuthorRepository authorRepository, + AuthorManager authorManager) + { + _bookRepository = bookRepository; + _authorRepository = authorRepository; + _authorManager = authorManager; + } + + public async Task SeedAsync(DataSeedContext context) + { + if (await _bookRepository.GetCountAsync() > 0) + { + return; + } + + // ADDED SEED DATA FOR AUTHORS + + await _authorRepository.InsertAsync( + await _authorManager.CreateAsync( + "George Orwell", + new DateTime(1903, 06, 25), + "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." + ) + ); + + await _authorRepository.InsertAsync( + await _authorManager.CreateAsync( + "Douglas Adams", + new DateTime(1952, 03, 11), + "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." + ) + ); + + await _bookRepository.InsertAsync( + new Book + { + Name = "1984", + Type = BookType.Dystopia, + PublishDate = new DateTime(1949, 6, 8), + Price = 19.84f + }, + autoSave: true + ); + + await _bookRepository.InsertAsync( + new Book + { + Name = "The Hitchhiker's Guide to the Galaxy", + Type = BookType.ScienceFiction, + PublishDate = new DateTime(1995, 9, 27), + Price = 42.0f + }, + autoSave: true + ); + } + } +} +```` + +## Testing the Author Application Service + +Finally, we can write some tests for the `IAuthorAppService`. Add a new class, named `AuthorAppService_Tests` in the `Authors` namespace (folder) of the `Acme.BookStore.Application.Tests` project: + +````csharp +using System; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Acme.BookStore.Authors +{ {{if DB=="Mongo"}} + [Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} + public class AuthorAppService_Tests : BookStoreApplicationTestBase + { + private readonly IAuthorAppService _authorAppService; + + public AuthorAppService_Tests() + { + _authorAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_All_Authors_Without_Any_Filter() + { + var result = await _authorAppService.GetListAsync(new GetAuthorListDto()); + + result.TotalCount.ShouldBeGreaterThanOrEqualTo(2); + result.Items.ShouldContain(author => author.Name == "George Orwell"); + result.Items.ShouldContain(author => author.Name == "Douglas Adams"); + } + + [Fact] + public async Task Should_Get_Filtered_Authors() + { + var result = await _authorAppService.GetListAsync( + new GetAuthorListDto {Filter = "George"}); + + result.TotalCount.ShouldBeGreaterThanOrEqualTo(1); + result.Items.ShouldContain(author => author.Name == "George Orwell"); + result.Items.ShouldNotContain(author => author.Name == "Douglas Adams"); + } + + [Fact] + public async Task Should_Create_A_New_Author() + { + var authorDto = await _authorAppService.CreateAsync( + new CreateAuthorDto + { + Name = "Edward Bellamy", + BirthDate = new DateTime(1850, 05, 22), + ShortBio = "Edward Bellamy was an American author..." + } + ); + + authorDto.Id.ShouldNotBe(Guid.Empty); + authorDto.Name.ShouldBe("Edward Bellamy"); + } + + [Fact] + public async Task Should_Not_Allow_To_Create_Duplicate_Author() + { + await Assert.ThrowsAsync(async () => + { + await _authorAppService.CreateAsync( + new CreateAuthorDto + { + Name = "Douglas Adams", + BirthDate = DateTime.Now, + ShortBio = "..." + } + ); + }); + } + + //TODO: Test other methods... + } +} +```` + +Created some tests for the application service methods, which should be clear to understand. + +## The Next Part + +See the [next part](Part-9.md) of this tutorial. \ No newline at end of file diff --git a/docs/en/Tutorials/Part-9.md b/docs/en/Tutorials/Part-9.md new file mode 100644 index 0000000000..1b9451700d --- /dev/null +++ b/docs/en/Tutorials/Part-9.md @@ -0,0 +1,854 @@ +# Web Application Development Tutorial - Part 9: Authors: User Interface +````json +//[doc-params] +{ + "UI": ["MVC","NG"], + "DB": ["EF","Mongo"] +} +```` +{{ +if UI == "MVC" + UI_Text="mvc" +else if UI == "NG" + UI_Text="angular" +else + UI_Text="?" +end +if DB == "EF" + DB_Text="Entity Framework Core" +else if DB == "Mongo" + DB_Text="MongoDB" +else + DB_Text="?" +end +}} + +## About This Tutorial + +In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: + +* **{{DB_Text}}** as the ORM provider. +* **{{UI_Value}}** as the UI Framework. + +This tutorial is organized as the following parts; + +- [Part 1: Creating the server side](Part-1.md) +- [Part 2: The book list page](Part-2.md) +- [Part 3: Creating, updating and deleting books](Part-3.md) +- [Part 4: Integration tests](Part-4.md) +- [Part 5: Authorization](Part-5.md) +- [Part 6: Authors: Domain layer](Part-6.md) +- [Part 7: Authors: Database Integration](Part-7.md) +- [Part 8: Authors: Application Layer](Part-8.md) +- **Part 9: Authors: User Interface (this part)** +- [Part 10: Book to Author Relation](Part-10.md) + +### Download the Source Code + +This tutorials has multiple versions based on your **UI** and **Database** preferences. We've prepared two combinations of the source code to be downloaded: + +* [MVC (Razor Pages) UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) + +## Introduction + +This part explains how to create a CRUD page for the `Author` entity introduced in previous parts. + +{{if UI == "MVC"}} + +## The Book List Page + +Create a new razor page, `Index.cshtml` under the `Pages/Authors` folder of the `Acme.BookStore.Web` project and change the content as given below. + +### Index.cshtml + +````html +@page +@using Acme.BookStore.Localization +@using Acme.BookStore.Permissions +@using Acme.BookStore.Web.Pages.Authors +@using Microsoft.AspNetCore.Authorization +@using Microsoft.Extensions.Localization +@inject IStringLocalizer L +@inject IAuthorizationService AuthorizationService +@model IndexModel + +@section scripts +{ + +} + + + + + + @L["Authors"] + + + @if (await AuthorizationService + .IsGrantedAsync(BookStorePermissions.Authors.Create)) + { + + } + + + + + + + +```` + +This is a simple page similar to the Books page we had created before. It imports a JavaScript file which will be introduced below. + +### IndexModel.cshtml.cs + +````csharp +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Acme.BookStore.Web.Pages.Authors +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} +```` + +### Index.js + +````js +$(function () { + var l = abp.localization.getResource('BookStore'); + var createModal = new abp.ModalManager(abp.appPath + 'Authors/CreateModal'); + var editModal = new abp.ModalManager(abp.appPath + 'Authors/EditModal'); + + var dataTable = $('#AuthorsTable').DataTable( + abp.libs.datatables.normalizeConfiguration({ + serverSide: true, + paging: true, + order: [[1, "asc"]], + searching: false, + scrollX: true, + ajax: abp.libs.datatables.createAjax(acme.bookStore.authors.author.getList), + columnDefs: [ + { + title: l('Actions'), + rowAction: { + items: + [ + { + text: l('Edit'), + visible: + abp.auth.isGranted('BookStore.Authors.Edit'), + action: function (data) { + editModal.open({ id: data.record.id }); + } + }, + { + text: l('Delete'), + visible: + abp.auth.isGranted('BookStore.Authors.Delete'), + confirmMessage: function (data) { + return l( + 'AuthorDeletionConfirmationMessage', + data.record.name + ); + }, + action: function (data) { + acme.bookStore.authors.author + .delete(data.record.id) + .then(function() { + abp.notify.info( + l('SuccessfullyDeleted') + ); + dataTable.ajax.reload(); + }); + } + } + ] + } + }, + { + title: l('Name'), + data: "name" + }, + { + title: l('BirthDate'), + data: "birthDate", + render: function (data) { + return luxon + .DateTime + .fromISO(data, { + locale: abp.localization.currentCulture.name + }).toLocaleString(); + } + } + ] + }) + ); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + editModal.onResult(function () { + dataTable.ajax.reload(); + }); + + $('#NewAuthorButton').click(function (e) { + e.preventDefault(); + createModal.open(); + }); +}); +```` + +Briefly, this JavaScript page; + +* Creates a Data table with `Actions`, `Name` and `BirthDate` columns. + * `Actions` column is used to add *Edit* and *Delete* actions. + * `BirthDate` provides a `render` function to format the `DateTime` value using the [luxon](https://moment.github.io/luxon/) library. +* Uses the `abp.ModalManager` to open *Create* and *Edit* modal forms. + +This code is very similar to the Books page created before, so we will not explain it more. + +### Localizations + +This page uses some localization keys we need to declare. Open the `en.json` file under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project and add the following entries: + +````json +"Menu:Authors": "Authors", +"Authors": "Authors", +"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?", +"BirthDate": "Birth date", +"NewAuthor": "New author" +```` + +Notice that we've added more keys. They will be used in the next sections. + +### Add to the Main Menu + +Open the `BookStoreMenuContributor.cs` in the `Menus` folder of the `Acme.BookStore.Web` project and add the following code in the end of the `ConfigureMainMenuAsync` method: + +````csharp +if (await context.IsGrantedAsync(BookStorePermissions.Authors.Default)) +{ + bookStoreMenu.AddItem(new ApplicationMenuItem( + "BooksStore.Authors", + l["Menu:Authors"], + url: "/Authors" + )); +} +```` + +### Run the Application + +Run and login to the application. **You can not see the menu item since you don't have permission yet.** Go to the `Identity/Roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**: + +![bookstore-author-permissions](images/bookstore-author-permissions.png) + +As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**: + +![bookstore-authors-page](images/bookstore-authors-page.png) + +The page is fully working except *New author* and *Actions/Edit* since we haven't implemented them yet. + +> **Tip**: If you run the `.DbMigrator` console application after defining a new permission, it automatically grants these new permissions to the admin role and you don't need to manually grant the permissions yourself. + +## Create Modal + +Create a new razor page, `CreateModal.cshtml` under the `Pages/Authors` folder of the `Acme.BookStore.Web` project and change the content as given below. + +### CreateModal.cshtml + +```html +@page +@using Acme.BookStore.Localization +@using Acme.BookStore.Web.Pages.Authors +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model CreateModalModel +@inject IStringLocalizer L +@{ + Layout = null; +} +
+ + + + + + + + + +
+``` + +We had used [dynamic forms](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) of the ABP Framework for the books page before. We could use the same approach here, but we wanted to show how to do it manually. Actually, not so manually, because we've used `abp-input` tag helper in this case to simplify creating the form elements. + +You can definitely use the standard Bootstrap HTML structure, but it requires to write a lot of code. `abp-input` automatically adds validation, localization and other standard elements based on the data type. + +### CreateModal.cshtml.cs + +```csharp +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Acme.BookStore.Authors; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +namespace Acme.BookStore.Web.Pages.Authors +{ + public class CreateModalModel : BookStorePageModel + { + [BindProperty] + public CreateAuthorViewModel Author { get; set; } + + private readonly IAuthorAppService _authorAppService; + + public CreateModalModel(IAuthorAppService authorAppService) + { + _authorAppService = authorAppService; + } + + public void OnGet() + { + Author = new CreateAuthorViewModel(); + } + + public async Task OnPostAsync() + { + var dto = ObjectMapper.Map(Author); + await _authorAppService.CreateAsync(dto); + return NoContent(); + } + + public class CreateAuthorViewModel + { + [Required] + [StringLength(AuthorConsts.MaxNameLength)] + public string Name { get; set; } + + [Required] + [DataType(DataType.Date)] + public DateTime BirthDate { get; set; } + + [TextArea] + public string ShortBio { get; set; } + } + } +} +``` + +This page model class simply injects and uses the `IAuthorAppService` to create a new author. The main difference between the book creation model class is that this one is declaring a new class, `CreateAuthorViewModel`, for the view model instead of re-using the `CreateAuthorDto`. + +The main reason of this decision was to show you how to use a different model class inside the page. But there is one more benefit: We added two attributes to the class members, which were not present in the `CreateAuthorDto`: + +* Added `[DataType(DataType.Date)]` attribute to the `BirthDate` which shows a date picker on the UI for this property. +* Added `[TextArea]` attribute to the `ShortBio` which shows a multi-line text area instead of a standard textbox. + +In this way, you can specialize the view model class based on your UI requirements without touching to the DTO. As a result of this decision, we have used `ObjectMapper` to map `CreateAuthorViewModel` to `CreateAuthorDto`. To be able to do that, you need to add a new mapping code to the `BookStoreWebAutoMapperProfile` constructor: + +````csharp +using Acme.BookStore.Authors; // ADDED NAMESPACE IMPORT +using Acme.BookStore.Books; +using AutoMapper; + +namespace Acme.BookStore.Web +{ + public class BookStoreWebAutoMapperProfile : Profile + { + public BookStoreWebAutoMapperProfile() + { + CreateMap(); + + // ADD a NEW MAPPING + CreateMap(); + } + } +} +```` + +"New author" button will work as expected and open a new model when you run the application again: + +![bookstore-new-author-modal](images/bookstore-new-author-modal.png) + +## Edit Modal + +Create a new razor page, `EditModal.cshtml` under the `Pages/Authors` folder of the `Acme.BookStore.Web` project and change the content as given below. + +### EditModal.cshtml + +````html +@page +@using Acme.BookStore.Localization +@using Acme.BookStore.Web.Pages.Authors +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model EditModalModel +@inject IStringLocalizer L +@{ + Layout = null; +} +
+ + + + + + + + + + +
+```` + +### EditModal.cshtml.cs + +```csharp +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Acme.BookStore.Authors; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; + +namespace Acme.BookStore.Web.Pages.Authors +{ + public class EditModalModel : BookStorePageModel + { + [BindProperty] + public EditAuthorViewModel Author { get; set; } + + private readonly IAuthorAppService _authorAppService; + + public EditModalModel(IAuthorAppService authorAppService) + { + _authorAppService = authorAppService; + } + + public async Task OnGetAsync(Guid id) + { + var authorDto = await _authorAppService.GetAsync(id); + Author = ObjectMapper.Map(authorDto); + } + + public async Task OnPostAsync() + { + await _authorAppService.UpdateAsync( + Author.Id, + ObjectMapper.Map(Author) + ); + + return NoContent(); + } + + public class EditAuthorViewModel + { + [HiddenInput] + public Guid Id { get; set; } + + [Required] + [StringLength(AuthorConsts.MaxNameLength)] + public string Name { get; set; } + + [Required] + [DataType(DataType.Date)] + public DateTime BirthDate { get; set; } + + [TextArea] + public string ShortBio { get; set; } + } + } +} +``` + +This class is similar to the `CreateModal.cshtml.cs` while there are some main differences; + +* Uses the `IAuthorAppService.GetAsync(...)` method to get the editing author from the application layer. +* `EditAuthorViewModel` has an additional `Id` property which is marked with the `[HiddenInput]` attribute that creates a hidden input for this property. + +This class requires to add two object mapping declarations to the `BookStoreWebAutoMapperProfile` class: + +```csharp +using Acme.BookStore.Authors; +using Acme.BookStore.Books; +using AutoMapper; + +namespace Acme.BookStore.Web +{ + public class BookStoreWebAutoMapperProfile : Profile + { + public BookStoreWebAutoMapperProfile() + { + CreateMap(); + + CreateMap(); + + // ADD THESE NEW MAPPINGS + CreateMap(); + CreateMap(); + } + } +} +``` + +That's all! You can run the application and try to edit an author. + +{{else if UI == "NG"}} + +## The Author List Page, Create & Delete Authors + +Run the following command line to create a new module, named `AuthorModule` in the root folder of the angular application: + +```bash +yarn ng generate module author --module app --routing --route authors +``` + +This command should produce the following output: + +```bash +> yarn ng generate module author --module app --routing --route authors + +yarn run v1.19.1 +$ ng generate module author --module app --routing --route authors +CREATE src/app/author/author-routing.module.ts (344 bytes) +CREATE src/app/author/author.module.ts (349 bytes) +CREATE src/app/author/author.component.html (21 bytes) +CREATE src/app/author/author.component.spec.ts (628 bytes) +CREATE src/app/author/author.component.ts (276 bytes) +CREATE src/app/author/author.component.scss (0 bytes) +UPDATE src/app/app-routing.module.ts (1396 bytes) +Done in 2.22s. +``` + +### AuthorModule + +Open the `/src/app/author/author.module.ts` and replace the content as shown below: + +```js +import { NgModule } from '@angular/core'; +import { SharedModule } from '../shared/shared.module'; +import { AuthorRoutingModule } from './author-routing.module'; +import { AuthorComponent } from './author.component'; +import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; + +@NgModule({ + declarations: [AuthorComponent], + imports: [SharedModule, AuthorRoutingModule, NgbDatepickerModule], +}) +export class AuthorModule {} +``` + +- Added the `SharedModule`. `SharedModule` exports some common modules needed to create user interfaces. +- `SharedModule` already exports the `CommonModule`, so we've removed the `CommonModule`. +- Added `NgbDatepickerModule` that will be used later on the author create and edit forms. + +### Menu Definition + +Open the `src/app/route.provider.ts` file and add the following menu definition: + +````js +{ + path: '/authors', + name: '::Menu:Authors', + parentName: '::Menu:BookStore', + layout: eLayoutType.application, + requiredPolicy: 'BookStore.Authors', +} +```` + +The final `configureRoutes` function declaration should be following: + +```js +function configureRoutes(routes: RoutesService) { + return () => { + routes.add([ + { + path: '/', + name: '::Menu:Home', + iconClass: 'fas fa-home', + order: 1, + layout: eLayoutType.application, + }, + { + path: '/book-store', + name: '::Menu:BookStore', + iconClass: 'fas fa-book', + order: 2, + layout: eLayoutType.application, + }, + { + path: '/books', + name: '::Menu:Books', + parentName: '::Menu:BookStore', + layout: eLayoutType.application, + requiredPolicy: 'BookStore.Books', + }, + { + path: '/authors', + name: '::Menu:Authors', + parentName: '::Menu:BookStore', + layout: eLayoutType.application, + requiredPolicy: 'BookStore.Authors', + }, + ]); + }; +} +``` + +### Service Proxy Generation + +[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides `generate-proxy` command that generates client proxies for your HTTP APIs to make easy to consume your HTTP APIs from the client side. Before running `generate-proxy` command, your host must be up and running. + +Run the following command in the `angular` folder: + +```bash +abp generate-proxy +``` + +This command generates the service proxy for the author service and the related model (DTO) classes: + +![bookstore-angular-service-proxy-author](images/bookstore-angular-service-proxy-author.png) + +### AuthorComponent + +Open the `/src/app/author/author.component.ts` file and replace the content as below: + +```js +import { Component, OnInit } from '@angular/core'; +import { ListService, PagedResultDto } from '@abp/ng.core'; +import { AuthorDto } from './models'; +import { AuthorService } from './services'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; +import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; + +@Component({ + selector: 'app-author', + templateUrl: './author.component.html', + styleUrls: ['./author.component.scss'], + providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], +}) +export class AuthorComponent implements OnInit { + author = { items: [], totalCount: 0 } as PagedResultDto; + + isModalOpen = false; + + form: FormGroup; + + selectedAuthor = new AuthorDto(); + + constructor( + public readonly list: ListService, + private authorService: AuthorService, + private fb: FormBuilder, + private confirmation: ConfirmationService + ) {} + + ngOnInit(): void { + const authorStreamCreator = (query) => this.authorService.getListByInput(query); + + this.list.hookToQuery(authorStreamCreator).subscribe((response) => { + this.author = response; + }); + } + + createAuthor() { + this.selectedAuthor = new AuthorDto(); + this.buildForm(); + this.isModalOpen = true; + } + + editAuthor(id: string) { + this.authorService.getById(id).subscribe((author) => { + this.selectedAuthor = author; + this.buildForm(); + this.isModalOpen = true; + }); + } + + buildForm() { + this.form = this.fb.group({ + name: [this.selectedAuthor.name || '', Validators.required], + birthDate: [ + this.selectedAuthor.birthDate ? new Date(this.selectedAuthor.birthDate) : null, + Validators.required, + ], + }); + } + + save() { + if (this.form.invalid) { + return; + } + + if (this.selectedAuthor.id) { + this.authorService + .updateByIdAndInput(this.form.value, this.selectedAuthor.id) + .subscribe(() => { + this.isModalOpen = false; + this.form.reset(); + this.list.get(); + }); + } else { + this.authorService.createByInput(this.form.value).subscribe(() => { + this.isModalOpen = false; + this.form.reset(); + this.list.get(); + }); + } + } + + delete(id: string) { + this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure') + .subscribe((status) => { + if (status === Confirmation.Status.confirm) { + this.authorService.deleteById(id).subscribe(() => this.list.get()); + } + }); + } +} +``` + +Open the `/src/app/author/author.component.html` and replace the content as below: + +````html +
+
+
+
+
+ {%{{{ '::Menu:Authors' | abpLocalization }}}%} +
+
+
+
+ +
+
+
+
+
+ + + +
+ +
+ + +
+
+
+
+ + + + {%{{{ row.birthDate | date }}}%} + + +
+
+
+ + + +

{%{{{ (selectedAuthor.id ? '::Edit' : '::NewAuthor') | abpLocalization }}}%}

+
+ + +
+
+ * + +
+ +
+ * + +
+
+
+ + + + + + +
+```` + +### Localizations + +This page uses some localization keys we need to declare. Open the `en.json` file under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project and add the following entries: + +````json +"Menu:Authors": "Authors", +"Authors": "Authors", +"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?", +"BirthDate": "Birth date", +"NewAuthor": "New author" +```` + +### Run the Application + +Run and login to the application. **You can not see the menu item since you don't have permission yet.** Go to the `identity/roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**: + +![bookstore-author-permissions](images/bookstore-author-permissions.png) + +As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**: + +![bookstore-authors-page](images/bookstore-angular-authors-page.png) + +That's all! This is a fully working CRUD page, you can create, edit and delete authors. + +> **Tip**: If you run the `.DbMigrator` console application after defining a new permission, it automatically grants these new permissions to the admin role and you don't need to manually grant the permissions yourself. + +{{end}} + +## The Next Part + +See the [next part](Part-10.md) of this tutorial. \ No newline at end of file diff --git a/docs/en/Tutorials/images/bookstore-add-migration-authors.png b/docs/en/Tutorials/images/bookstore-add-migration-authors.png new file mode 100644 index 0000000000..c24e33c628 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-add-migration-authors.png differ diff --git a/docs/en/Tutorials/images/bookstore-added-author-to-book-list-angular.png b/docs/en/Tutorials/images/bookstore-added-author-to-book-list-angular.png new file mode 100644 index 0000000000..9fba94d5bf Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-added-author-to-book-list-angular.png differ diff --git a/docs/en/Tutorials/images/bookstore-added-author-to-book-list.png b/docs/en/Tutorials/images/bookstore-added-author-to-book-list.png new file mode 100644 index 0000000000..f318ddfd31 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-added-author-to-book-list.png differ diff --git a/docs/en/Tutorials/images/bookstore-added-authors-to-modals.png b/docs/en/Tutorials/images/bookstore-added-authors-to-modals.png new file mode 100644 index 0000000000..5fe4046794 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-added-authors-to-modals.png differ diff --git a/docs/en/Tutorials/images/bookstore-angular-authors-page.png b/docs/en/Tutorials/images/bookstore-angular-authors-page.png new file mode 100644 index 0000000000..bbd865e44b Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-angular-authors-page.png differ diff --git a/docs/en/Tutorials/images/bookstore-angular-service-proxy-author.png b/docs/en/Tutorials/images/bookstore-angular-service-proxy-author.png new file mode 100644 index 0000000000..ac231ef7a8 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-angular-service-proxy-author.png differ diff --git a/docs/en/Tutorials/images/bookstore-author-domain-layer.png b/docs/en/Tutorials/images/bookstore-author-domain-layer.png new file mode 100644 index 0000000000..fde53834bb Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-author-domain-layer.png differ diff --git a/docs/en/Tutorials/images/bookstore-author-permissions.png b/docs/en/Tutorials/images/bookstore-author-permissions.png new file mode 100644 index 0000000000..a20093d2c4 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-author-permissions.png differ diff --git a/docs/en/Tutorials/images/bookstore-authors-page.png b/docs/en/Tutorials/images/bookstore-authors-page.png new file mode 100644 index 0000000000..6b8959df54 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-authors-page.png differ diff --git a/docs/en/Tutorials/images/bookstore-new-author-modal.png b/docs/en/Tutorials/images/bookstore-new-author-modal.png new file mode 100644 index 0000000000..dda0dab4c1 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-new-author-modal.png differ diff --git a/docs/en/UI/Angular/Localization.md b/docs/en/UI/Angular/Localization.md index 78822a16de..1ee68ace82 100644 --- a/docs/en/UI/Angular/Localization.md +++ b/docs/en/UI/Angular/Localization.md @@ -193,6 +193,32 @@ import { Component } from '@angular/core'; export class AppComponent {} ``` +## Mapping of Culture Name to Angular Locale File Name + +Some of the culture names defined in .NET do not match Angular locales. In such cases, the Angular app throws an error like below at runtime: + +![locale-error](./images/locale-error.png) + +If you see an error like this, you should pass the `cultureNameToLocaleFileNameMapping` property like below to CoreModule's forRoot static method. + +```js +// app.module.ts + +@NgModule({ + imports: [ + // other imports + CoreModule.forRoot({ + // other options + cultureNameToLocaleFileNameMapping: { + "DotnetCultureName": "AngularLocaleFileName", + "pt-BR": "pt" // example + } + }) + //... +``` + +See [all locale files in Angular](https://github.com/angular/angular/tree/master/packages/common/locales). + ## See Also @@ -200,4 +226,4 @@ export class AppComponent {} ## What's Next? -* [Permission Management](./Permission-Management.md) \ No newline at end of file +* [Permission Management](./Permission-Management.md) diff --git a/docs/en/UI/Angular/images/locale-error.png b/docs/en/UI/Angular/images/locale-error.png new file mode 100644 index 0000000000..de385a59c0 Binary files /dev/null and b/docs/en/UI/Angular/images/locale-error.png differ diff --git a/docs/en/UI/AspNetCore/Dynamic-JavaScript-Proxies.md b/docs/en/UI/AspNetCore/Dynamic-JavaScript-Proxies.md new file mode 100644 index 0000000000..391a63a910 --- /dev/null +++ b/docs/en/UI/AspNetCore/Dynamic-JavaScript-Proxies.md @@ -0,0 +1,3 @@ +# Dynamic JavaScript HTTP API Proxies + +TODO \ No newline at end of file diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 0904731d88..09288e013b 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -47,6 +47,26 @@ { "text": "5: Authorization", "path": "Tutorials/Part-5.md" + }, + { + "text": "6: Authors: Domain layer", + "path": "Tutorials/Part-6.md" + }, + { + "text": "7: Authors: Database Integration", + "path": "Tutorials/Part-7.md" + }, + { + "text": "8: Authors: Application Layer", + "path": "Tutorials/Part-8.md" + }, + { + "text": "9: Authors: User Interface", + "path": "Tutorials/Part-9.md" + }, + { + "text": "10: Book to Author Relation", + "path": "Tutorials/Part-10.md" } ] } diff --git a/docs/zh-Hans/Blob-Storing-Aliyun.md b/docs/zh-Hans/Blob-Storing-Aliyun.md index adeb902830..ba66d55104 100644 --- a/docs/zh-Hans/Blob-Storing-Aliyun.md +++ b/docs/zh-Hans/Blob-Storing-Aliyun.md @@ -46,16 +46,18 @@ Configure(options => * **AccessKeyId** ([NotNull]string): 云账号AccessKey是访问阿里云API的密钥,具有该账户完全的权限,请你务必妥善保管!强烈建议遵循[阿里云安全最佳实践](https://help.aliyun.com/document_detail/102600.html),使用RAM子用户AccessKey来进行API调用. * **AccessKeySecret** ([NotNull]string): 同上. * **Endpoint** ([NotNull]string): Endpoint表示OSS对外服务的访问域名. [访问域名和数据中心](https://help.aliyun.com/document_detail/31837.html) +* **UseSecurityTokenService** (bool): 是否使用STS临时授权访问OSS,默认false. [STS临时授权访问OSS](https://help.aliyun.com/document_detail/100624.html) * **RegionId** (string): STS服务的接入地址,每个地址的功能都相同,请尽量在同地域进行调用. [接入地址](https://help.aliyun.com/document_detail/66053.html) -* **RoleArn** ([NotNull]string): STS所需角色ARN. [STS临时授权访问OSS](https://help.aliyun.com/document_detail/100624.html) +* **RoleArn** ([NotNull]string): STS所需角色ARN. * **RoleSessionName** ([NotNull]string): 用来标识临时访问凭证的名称,建议使用不同的应用程序用户来区分. * **Policy** (string): 在扮演角色的时候额外添加的权限限制. 请参见[基于RAM Policy的权限控制](https://help.aliyun.com/document_detail/100680.html). -* **DurationSeconds** (int): 设置临时访问凭证的有效期,单位是s,最小为900,最大为3600. **注**:为0则使用子账号操作OSS. +* **DurationSeconds** (int): 设置临时访问凭证的有效期,单位是s,最小为900,最大为3600. * **ContainerName** (string): 你可以在aliyun中指定容器名称. 如果没有指定它将使用 `BlogContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aliyun有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://help.aliyun.com/knowledge_detail/39668.html): * 只能包含小写字母,数字和短横线(-) * 必须以小写字母和数字开头和结尾 * Bucket名称的长度限制在**3**到**63**个字符之间 * **CreateContainerIfNotExists** (bool): 默认值为 `false`, 如果aliyun中不存在容器, `AliyunBlobProvider` 将尝试创建它. +* **TemporaryCredentialsCacheKey** (bool): STS凭证缓存Key,默认Guid.NewGuid().ToString("N"). ## Aliyun BLOB 名称计算器 diff --git a/docs/zh-Hans/Tutorials/Part-3.md b/docs/zh-Hans/Tutorials/Part-3.md index cded954e6b..c9eb3bac78 100644 --- a/docs/zh-Hans/Tutorials/Part-3.md +++ b/docs/zh-Hans/Tutorials/Part-3.md @@ -23,7 +23,7 @@ end ### 关于本教程 -这是ASP.NET Core{{UI_Value}}系列教程的第二章. 共有三章: +这是ASP.NET Core{{UI_Value}}系列教程的第三章. 共有三章: - [Part-1: 创建项目和书籍列表页面](Part-1.md) - [Part 2: 创建,编辑,删除书籍](Part-2.md) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs index efd3af2d2c..711e8dfb6c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelper.cs @@ -36,6 +36,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form public string Value { get; set; } + public bool SuppressLabel { get; set; } + public AbpInputTagHelper(AbpInputTagHelperService tagHelperService) : base(tagHelperService) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs index 4ddf44d2d6..58a88c676f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs @@ -247,7 +247,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form protected virtual async Task GetLabelAsHtmlAsync(TagHelperContext context, TagHelperOutput output, TagHelperOutput inputTag, bool isCheckbox) { - if (IsOutputHidden(inputTag)) + if (IsOutputHidden(inputTag) || TagHelper.SuppressLabel) { return ""; } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Toolbar/LanguageSwitch/LanguageSwitchViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Toolbar/LanguageSwitch/LanguageSwitchViewComponent.cs index c6921d3210..3dd1ba2b9d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Toolbar/LanguageSwitch/LanguageSwitchViewComponent.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Toolbar/LanguageSwitch/LanguageSwitchViewComponent.cs @@ -1,7 +1,9 @@ -using System.Globalization; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RequestLocalization; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Localization; namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Themes.Basic.Components.Toolbar.LanguageSwitch @@ -23,12 +25,32 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Themes.Basic.Components.Toolbar CultureInfo.CurrentUICulture.Name ); + if (currentLanguage == null) + { + var abpRequestLocalizationOptionsProvider = HttpContext.RequestServices.GetRequiredService(); + var localizationOptions = await abpRequestLocalizationOptionsProvider.GetLocalizationOptionsAsync(); + if (localizationOptions.DefaultRequestCulture != null) + { + currentLanguage = new LanguageInfo( + localizationOptions.DefaultRequestCulture.Culture.Name, + localizationOptions.DefaultRequestCulture.UICulture.Name, + localizationOptions.DefaultRequestCulture.UICulture.DisplayName); + } + else + { + currentLanguage = new LanguageInfo( + CultureInfo.CurrentCulture.Name, + CultureInfo.CurrentUICulture.Name, + CultureInfo.CurrentUICulture.DisplayName); + } + } + var model = new LanguageSwitchViewComponentModel { CurrentLanguage = currentLanguage, OtherLanguages = languages.Where(l => l != currentLanguage).ToList() }; - + return View("~/Themes/Basic/Components/Toolbar/LanguageSwitch/Default.cshtml", model); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml index b4ae34d8fb..5fcd5951cd 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml @@ -39,8 +39,10 @@ @(ViewBag.Title == null ? BrandingProvider.AppName : ViewBag.Title) - - + @if (ViewBag.Description != null) + { + + } @await RenderSectionAsync("styles", false) diff --git a/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/AliyunBlobProviderConfiguration.cs b/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/AliyunBlobProviderConfiguration.cs index 058f65412c..757ad4b54c 100644 --- a/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/AliyunBlobProviderConfiguration.cs +++ b/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/AliyunBlobProviderConfiguration.cs @@ -25,6 +25,12 @@ namespace Volo.Abp.BlobStoring.Aliyun set => _containerConfiguration.SetConfiguration(AliyunBlobProviderConfigurationNames.Endpoint, Check.NotNullOrWhiteSpace(value, nameof(value))); } + public bool UseSecurityTokenService + { + get => _containerConfiguration.GetConfigurationOrDefault(AliyunBlobProviderConfigurationNames.UseSecurityTokenService, false); + set => _containerConfiguration.SetConfiguration(AliyunBlobProviderConfigurationNames.UseSecurityTokenService, value); + } + public string RegionId { get => _containerConfiguration.GetConfiguration(AliyunBlobProviderConfigurationNames.RegionId); @@ -88,18 +94,19 @@ namespace Volo.Abp.BlobStoring.Aliyun set => _containerConfiguration.SetConfiguration(AliyunBlobProviderConfigurationNames.CreateContainerIfNotExists, value); } - private readonly BlobContainerConfiguration _containerConfiguration; - - public AliyunBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration) + private readonly string _temporaryCredentialsCacheKey; + public string TemporaryCredentialsCacheKey { - _containerConfiguration = containerConfiguration; + get => _containerConfiguration.GetConfigurationOrDefault(AliyunBlobProviderConfigurationNames.TemporaryCredentialsCacheKey, _temporaryCredentialsCacheKey); + set => _containerConfiguration.SetConfiguration(AliyunBlobProviderConfigurationNames.TemporaryCredentialsCacheKey, value); } + private readonly BlobContainerConfiguration _containerConfiguration; - public string ToKeyString() + public AliyunBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration) { - Uri uPoint = new Uri(Endpoint); - return $"blobstoring:aliyun:id:{AccessKeyId},sec:{AccessKeySecret},ept:{uPoint.Host.ToLower()},rid:{RegionId},ra:{RoleArn},rsn:{RoleSessionName},pl:{Policy}"; + _containerConfiguration = containerConfiguration; + _temporaryCredentialsCacheKey = Guid.NewGuid().ToString("N"); } } } diff --git a/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/AliyunBlobProviderConfigurationNames.cs b/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/AliyunBlobProviderConfigurationNames.cs index c889b374cc..0f8585f12b 100644 --- a/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/AliyunBlobProviderConfigurationNames.cs +++ b/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/AliyunBlobProviderConfigurationNames.cs @@ -5,12 +5,14 @@ public const string AccessKeyId = "Aliyun.AccessKeyId"; public const string AccessKeySecret = "Aliyun.AccessKeySecret"; public const string Endpoint = "Aliyun.Endpoint"; + public const string UseSecurityTokenService = "Aliyun.UseSecurityTokenService"; public const string RegionId = "Aliyun.RegionId"; public const string RoleArn = "Aliyun.RoleArn"; public const string RoleSessionName = "Aliyun.RoleSessionName"; public const string DurationSeconds = "Aliyun.DurationSeconds"; public const string Policy = "Aliyun.Policy"; - public const string ContainerName = "Aliyun:ContainerName"; + public const string ContainerName = "Aliyun.ContainerName"; public const string CreateContainerIfNotExists = "Aliyun.CreateContainerIfNotExists"; + public const string TemporaryCredentialsCacheKey = "Aliyun.TemporaryCredentialsCacheKey"; } } diff --git a/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/AssumeRoleCredentialsCacheItem.cs b/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/AliyunTemporaryCredentialsCacheItem.cs similarity index 66% rename from framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/AssumeRoleCredentialsCacheItem.cs rename to framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/AliyunTemporaryCredentialsCacheItem.cs index e0d75f2954..508d94b743 100644 --- a/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/AssumeRoleCredentialsCacheItem.cs +++ b/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/AliyunTemporaryCredentialsCacheItem.cs @@ -6,8 +6,7 @@ using Volo.Abp.Caching; namespace Volo.Abp.BlobStoring.Aliyun { [Serializable] - [CacheName("AssumeRoleCredentials")] - public class AssumeRoleCredentialsCacheItem + public class AliyunTemporaryCredentialsCacheItem { public string AccessKeyId { get; set; } @@ -15,12 +14,12 @@ namespace Volo.Abp.BlobStoring.Aliyun public string SecurityToken { get; set; } - public AssumeRoleCredentialsCacheItem() + public AliyunTemporaryCredentialsCacheItem() { } - public AssumeRoleCredentialsCacheItem(string accessKeyId,string accessKeySecret,string securityToken) + public AliyunTemporaryCredentialsCacheItem(string accessKeyId,string accessKeySecret,string securityToken) { AccessKeyId = accessKeyId; AccessKeySecret = accessKeySecret; diff --git a/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/DefaultOssClientFactory.cs b/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/DefaultOssClientFactory.cs index 8f69603f7c..ea0c4cbf28 100644 --- a/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/DefaultOssClientFactory.cs +++ b/framework/src/Volo.Abp.BlobStoring.Aliyun/Volo/Abp/BlobStoring/Aliyun/DefaultOssClientFactory.cs @@ -6,8 +6,11 @@ using Aliyun.OSS; using Microsoft.Extensions.Caching.Distributed; using System; using System.Collections.Generic; +using System.Threading.Tasks; using Volo.Abp.Caching; using Volo.Abp.DependencyInjection; +using Volo.Abp.Security.Encryption; +using static Aliyun.Acs.Core.Auth.Sts.AssumeRoleResponse; namespace Volo.Abp.BlobStoring.Aliyun { @@ -16,53 +19,82 @@ namespace Volo.Abp.BlobStoring.Aliyun /// public class DefaultOssClientFactory : IOssClientFactory, ITransientDependency { - protected IDistributedCache Cache { get; } + protected IDistributedCache Cache { get; } + + protected IStringEncryptionService StringEncryptionService { get; } + public DefaultOssClientFactory( - IDistributedCache cache) + IDistributedCache cache, + IStringEncryptionService stringEncryptionService) { Cache = cache; + StringEncryptionService = stringEncryptionService; } - public virtual IOss Create(AliyunBlobProviderConfiguration aliyunConfig) + public virtual IOss Create(AliyunBlobProviderConfiguration configuration) { - //Sub-account - if (aliyunConfig.DurationSeconds <= 0) + Check.NotNullOrWhiteSpace(configuration.AccessKeyId, nameof(configuration.AccessKeyId)); + Check.NotNullOrWhiteSpace(configuration.AccessKeySecret, nameof(configuration.AccessKeySecret)); + Check.NotNullOrWhiteSpace(configuration.Endpoint, nameof(configuration.Endpoint)); + if (configuration.UseSecurityTokenService) { - return new OssClient(aliyunConfig.Endpoint, aliyunConfig.AccessKeyId, aliyunConfig.AccessKeySecret); + //STS temporary authorization to access OSS + return GetSecurityTokenClient(configuration); } - else + //Sub-account + return new OssClient(configuration.Endpoint, configuration.AccessKeyId, configuration.AccessKeySecret); + } + + protected virtual IOss GetSecurityTokenClient(AliyunBlobProviderConfiguration configuration) + { + Check.NotNullOrWhiteSpace(configuration.RoleArn, nameof(configuration.RoleArn)); + Check.NotNullOrWhiteSpace(configuration.RoleSessionName, nameof(configuration.RoleSessionName)); + var cacheItem = Cache.Get(configuration.TemporaryCredentialsCacheKey); + if (cacheItem == null) { - //STS temporary authorization to access OSS - var key = aliyunConfig.ToKeyString(); - var cacheItem = Cache.Get(key); - if (cacheItem == null) + IClientProfile profile = DefaultProfile.GetProfile( + configuration.RegionId, + configuration.AccessKeyId, + configuration.AccessKeySecret); + DefaultAcsClient client = new DefaultAcsClient(profile); + AssumeRoleRequest request = new AssumeRoleRequest { - IClientProfile profile = DefaultProfile.GetProfile( - aliyunConfig.RegionId, - aliyunConfig.AccessKeyId, - aliyunConfig.AccessKeySecret); - DefaultAcsClient client = new DefaultAcsClient(profile); - AssumeRoleRequest request = new AssumeRoleRequest - { - AcceptFormat = FormatType.JSON, - //eg:acs:ram::$accountID:role/$roleName - RoleArn = aliyunConfig.RoleArn, - RoleSessionName = aliyunConfig.RoleSessionName, - //Set the validity period of the temporary access credential, the unit is s, the minimum is 900, and the maximum is 3600. default 3600 - DurationSeconds = aliyunConfig.DurationSeconds, - //Set additional permission policy of Token; when acquiring Token, further reduce the permission of Token by setting an additional permission policy - Policy = aliyunConfig.Policy.IsNullOrEmpty() ? null : aliyunConfig.Policy, - }; - var response = client.GetAcsResponse(request); - cacheItem = new AssumeRoleCredentialsCacheItem(response.Credentials.AccessKeyId, response.Credentials.AccessKeySecret, response.Credentials.SecurityToken); - Cache.Set(key, cacheItem, new DistributedCacheEntryOptions() - { - //Subtract 10 seconds of network request time. - AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(aliyunConfig.DurationSeconds - 10) - }); - } - return new OssClient(aliyunConfig.Endpoint, cacheItem.AccessKeyId, cacheItem.AccessKeySecret, cacheItem.SecurityToken); + AcceptFormat = FormatType.JSON, + //eg:acs:ram::$accountID:role/$roleName + RoleArn = configuration.RoleArn, + RoleSessionName = configuration.RoleSessionName, + //Set the validity period of the temporary access credential, the unit is s, the minimum is 900, and the maximum is 3600. default 3600 + DurationSeconds = configuration.DurationSeconds, + //Set additional permission policy of Token; when acquiring Token, further reduce the permission of Token by setting an additional permission policy + Policy = configuration.Policy.IsNullOrEmpty() ? null : configuration.Policy, + }; + var response = client.GetAcsResponse(request); + cacheItem = SetTemporaryCredentialsCache(configuration, response.Credentials); } + return new OssClient( + configuration.Endpoint, + StringEncryptionService.Decrypt(cacheItem.AccessKeyId), + StringEncryptionService.Decrypt(cacheItem.AccessKeySecret), + StringEncryptionService.Decrypt(cacheItem.SecurityToken)); + } + + private AliyunTemporaryCredentialsCacheItem SetTemporaryCredentialsCache( + AliyunBlobProviderConfiguration configuration, + AssumeRole_Credentials credentials) + { + var temporaryCredentialsCache = new AliyunTemporaryCredentialsCacheItem( + StringEncryptionService.Encrypt(credentials.AccessKeyId), + StringEncryptionService.Encrypt(credentials.AccessKeySecret), + StringEncryptionService.Encrypt(credentials.SecurityToken)); + + Cache.Set(configuration.TemporaryCredentialsCacheKey, temporaryCredentialsCache, + new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(configuration.DurationSeconds - 10) + }); + + return temporaryCredentialsCache; } + } } diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs index ba59bf039e..d133ac110b 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs @@ -5,12 +5,14 @@ using Volo.Abp.Modularity; using Volo.Abp.MultiTenancy; using Volo.Abp.Serialization; using Volo.Abp.Threading; +using Volo.Abp.Uow; namespace Volo.Abp.Caching { [DependsOn( typeof(AbpThreadingModule), typeof(AbpSerializationModule), + typeof(AbpUnitOfWorkModule), typeof(AbpMultiTenancyModule), typeof(AbpJsonModule))] public class AbpCachingModule : AbpModule 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 5bb1dd2481..b26feec136 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs @@ -14,6 +14,7 @@ using Volo.Abp.DependencyInjection; using Volo.Abp.ExceptionHandling; using Volo.Abp.MultiTenancy; using Volo.Abp.Threading; +using Volo.Abp.Uow; namespace Volo.Abp.Caching { @@ -30,13 +31,15 @@ namespace Volo.Abp.Caching ICancellationTokenProvider cancellationTokenProvider, IDistributedCacheSerializer serializer, IDistributedCacheKeyNormalizer keyNormalizer, - IHybridServiceScopeFactory serviceScopeFactory) : base( - distributedCacheOption: distributedCacheOption, - cache: cache, - cancellationTokenProvider: cancellationTokenProvider, - serializer: serializer, - keyNormalizer: keyNormalizer, - serviceScopeFactory: serviceScopeFactory) + IHybridServiceScopeFactory serviceScopeFactory, + IUnitOfWorkManager unitOfWorkManager) : base( + distributedCacheOption: distributedCacheOption, + cache: cache, + cancellationTokenProvider: cancellationTokenProvider, + serializer: serializer, + keyNormalizer: keyNormalizer, + serviceScopeFactory: serviceScopeFactory, + unitOfWorkManager:unitOfWorkManager) { } } @@ -50,6 +53,8 @@ namespace Volo.Abp.Caching public class DistributedCache : IDistributedCache where TCacheItem : class { + public const string UowCacheName = "AbpDistributedCache"; + public ILogger> Logger { get; set; } protected string CacheName { get; set; } @@ -66,6 +71,8 @@ namespace Volo.Abp.Caching protected IHybridServiceScopeFactory ServiceScopeFactory { get; } + protected IUnitOfWorkManager UnitOfWorkManager { get; } + protected SemaphoreSlim SyncSemaphore { get; } protected DistributedCacheEntryOptions DefaultCacheOptions; @@ -78,7 +85,8 @@ namespace Volo.Abp.Caching ICancellationTokenProvider cancellationTokenProvider, IDistributedCacheSerializer serializer, IDistributedCacheKeyNormalizer keyNormalizer, - IHybridServiceScopeFactory serviceScopeFactory) + IHybridServiceScopeFactory serviceScopeFactory, + IUnitOfWorkManager unitOfWorkManager) { _distributedCacheOption = distributedCacheOption.Value; Cache = cache; @@ -87,6 +95,7 @@ namespace Volo.Abp.Caching Serializer = serializer; KeyNormalizer = keyNormalizer; ServiceScopeFactory = serviceScopeFactory; + UnitOfWorkManager = unitOfWorkManager; SyncSemaphore = new SemaphoreSlim(1, 1); @@ -134,13 +143,24 @@ namespace Volo.Abp.Caching /// /// The key of cached item to be retrieved from the cache. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. /// The cache item, or null. public virtual TCacheItem Get( TCacheKey key, - bool? hideErrors = null) + bool? hideErrors = null, + bool considerUow = false) { hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + if (ShouldConsiderUow(considerUow)) + { + var value = GetUnitOfWorkCache().GetOrDefault(key)?.GetUnRemovedValueOrNull(); + if (value != null) + { + return value; + } + } + byte[] cachedBytes; try @@ -163,7 +183,8 @@ namespace Volo.Abp.Caching public virtual KeyValuePair[] GetMany( IEnumerable keys, - bool? hideErrors = null) + bool? hideErrors = null, + bool considerUow = false) { var keyArray = keys.ToArray(); @@ -172,16 +193,39 @@ namespace Volo.Abp.Caching { return GetManyFallback( keyArray, - hideErrors + hideErrors, + considerUow ); } + var notCachedKeys = new List(); + var cachedValues = new List>(); + if (ShouldConsiderUow(considerUow)) + { + var uowCache = GetUnitOfWorkCache(); + foreach (var key in keyArray) + { + var value = uowCache.GetOrDefault(key)?.GetUnRemovedValueOrNull(); + if (value != null) + { + cachedValues.Add(new KeyValuePair(key, value)); + } + } + + notCachedKeys = keyArray.Except(cachedValues.Select(x => x.Key)).ToList(); + if (!notCachedKeys.Any()) + { + return cachedValues.ToArray(); + } + } + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; byte[][] cachedBytes; + var readKeys = notCachedKeys.Any() ? notCachedKeys.ToArray() : keyArray; try { - cachedBytes = cacheSupportsMultipleItems.GetMany(keyArray.Select(NormalizeKey)); + cachedBytes = cacheSupportsMultipleItems.GetMany(readKeys.Select(NormalizeKey)); } catch (Exception ex) { @@ -193,13 +237,14 @@ namespace Volo.Abp.Caching throw; } - - return ToCacheItems(cachedBytes, keyArray); + + return cachedValues.Concat(ToCacheItems(cachedBytes, readKeys)).ToArray(); } protected virtual KeyValuePair[] GetManyFallback( TCacheKey[] keys, - bool? hideErrors = null) + bool? hideErrors = null, + bool considerUow = false) { hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; @@ -208,7 +253,7 @@ namespace Volo.Abp.Caching return keys .Select(key => new KeyValuePair( key, - Get(key, hideErrors: false) + Get(key, false, considerUow) ) ).ToArray(); } @@ -227,6 +272,7 @@ namespace Volo.Abp.Caching public virtual async Task[]> GetManyAsync( IEnumerable keys, bool? hideErrors = null, + bool considerUow = false, CancellationToken token = default) { var keyArray = keys.ToArray(); @@ -237,17 +283,41 @@ namespace Volo.Abp.Caching return await GetManyFallbackAsync( keyArray, hideErrors, + considerUow, token ); } + var notCachedKeys = new List(); + var cachedValues = new List>(); + if (ShouldConsiderUow(considerUow)) + { + var uowCache = GetUnitOfWorkCache(); + foreach (var key in keyArray) + { + var value = uowCache.GetOrDefault(key)?.GetUnRemovedValueOrNull(); + if (value != null) + { + cachedValues.Add(new KeyValuePair(key, value)); + } + } + + notCachedKeys = keyArray.Except(cachedValues.Select(x => x.Key)).ToList(); + if (!notCachedKeys.Any()) + { + return cachedValues.ToArray(); + } + } + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; byte[][] cachedBytes; + var readKeys = notCachedKeys.Any() ? notCachedKeys.ToArray() : keyArray; + try { cachedBytes = await cacheSupportsMultipleItems.GetManyAsync( - keyArray.Select(NormalizeKey), + readKeys.Select(NormalizeKey), CancellationTokenProvider.FallbackToProvider(token) ); } @@ -261,13 +331,14 @@ namespace Volo.Abp.Caching throw; } - - return ToCacheItems(cachedBytes, keyArray); + + return cachedValues.Concat(ToCacheItems(cachedBytes, readKeys)).ToArray(); } protected virtual async Task[]> GetManyFallbackAsync( TCacheKey[] keys, bool? hideErrors = null, + bool considerUow = false, CancellationToken token = default) { hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; @@ -280,7 +351,7 @@ namespace Volo.Abp.Caching { result.Add(new KeyValuePair( key, - await GetAsync(key, false, token)) + await GetAsync(key, false, considerUow, token: token)) ); } @@ -303,15 +374,26 @@ namespace Volo.Abp.Caching /// /// The key of cached item to be retrieved from the cache. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. /// The for the task. /// The cache item, or null. public virtual async Task GetAsync( TCacheKey key, bool? hideErrors = null, + bool considerUow = false, CancellationToken token = default) { hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + if (ShouldConsiderUow(considerUow)) + { + var value = GetUnitOfWorkCache().GetOrDefault(key)?.GetUnRemovedValueOrNull(); + if (value != null) + { + return value; + } + } + byte[] cachedBytes; try @@ -348,14 +430,16 @@ namespace Volo.Abp.Caching /// The factory delegate is used to provide the cache item when no cache item is found for the given . /// The cache options for the factory delegate. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. /// The cache item. public virtual TCacheItem GetOrAdd( TCacheKey key, Func factory, Func optionsFactory = null, - bool? hideErrors = null) + bool? hideErrors = null, + bool considerUow = false) { - var value = Get(key, hideErrors); + var value = Get(key, hideErrors, considerUow); if (value != null) { return value; @@ -363,14 +447,28 @@ namespace Volo.Abp.Caching using (SyncSemaphore.Lock()) { - value = Get(key, hideErrors); + value = Get(key, hideErrors, considerUow); if (value != null) { return value; } value = factory(); - Set(key, value, optionsFactory?.Invoke(), hideErrors); + + if (ShouldConsiderUow(considerUow)) + { + var uowCache = GetUnitOfWorkCache(); + if (uowCache.TryGetValue(key, out var item)) + { + item.SetValue(value); + } + else + { + uowCache.Add(key, new UnitOfWorkCacheItem(value)); + } + } + + Set(key, value, optionsFactory?.Invoke(), hideErrors, considerUow); } return value; @@ -384,6 +482,7 @@ namespace Volo.Abp.Caching /// The factory delegate is used to provide the cache item when no cache item is found for the given . /// The cache options for the factory delegate. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. /// The for the task. /// The cache item. public virtual async Task GetOrAddAsync( @@ -391,10 +490,11 @@ namespace Volo.Abp.Caching Func> factory, Func optionsFactory = null, bool? hideErrors = null, + bool considerUow = false, CancellationToken token = default) { token = CancellationTokenProvider.FallbackToProvider(token); - var value = await GetAsync(key, hideErrors, token); + var value = await GetAsync(key, hideErrors, considerUow, token); if (value != null) { return value; @@ -402,14 +502,28 @@ namespace Volo.Abp.Caching using (await SyncSemaphore.LockAsync(token)) { - value = await GetAsync(key, hideErrors, token); + value = await GetAsync(key, hideErrors, considerUow, token); if (value != null) { return value; } value = await factory(); - await SetAsync(key, value, optionsFactory?.Invoke(), hideErrors, token); + + if (ShouldConsiderUow(considerUow)) + { + var uowCache = GetUnitOfWorkCache(); + if (uowCache.TryGetValue(key, out var item)) + { + item.SetValue(value); + } + else + { + uowCache.Add(key, new UnitOfWorkCacheItem(value)); + } + } + + await SetAsync(key, value, optionsFactory?.Invoke(), hideErrors, considerUow, token); } return value; @@ -422,34 +536,62 @@ namespace Volo.Abp.Caching /// The cache item value to set in the cache. /// The cache options for the value. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. public virtual void Set( TCacheKey key, TCacheItem value, DistributedCacheEntryOptions options = null, - bool? hideErrors = null) + bool? hideErrors = null, + bool considerUow = false) { - hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; - - try + void SetRealCache() { - Cache.Set( - NormalizeKey(key), - Serializer.Serialize(value), - options ?? DefaultCacheOptions - ); + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + Cache.Set( + NormalizeKey(key), + Serializer.Serialize(value), + options ?? DefaultCacheOptions + ); + } + catch (Exception ex) + { + if (hideErrors == true) + { + HandleException(ex); + return; + } + + throw; + } } - catch (Exception ex) + + if (ShouldConsiderUow(considerUow)) { - if (hideErrors == true) + var uowCache = GetUnitOfWorkCache(); + if (uowCache.TryGetValue(key, out _)) { - HandleException(ex); - return; + uowCache[key].SetValue(value); + } + else + { + uowCache.Add(key, new UnitOfWorkCacheItem(value)); } - throw; + // ReSharper disable once PossibleNullReferenceException + UnitOfWorkManager.Current.OnCompleted(() => + { + SetRealCache(); + return Task.CompletedTask; + }); + } + else + { + SetRealCache(); } } - /// /// Sets the cache item value for the provided key. /// @@ -457,6 +599,7 @@ namespace Volo.Abp.Caching /// The cache item value to set in the cache. /// The cache options for the value. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. /// The for the task. /// The indicating that the operation is asynchronous. public virtual async Task SetAsync( @@ -464,35 +607,60 @@ namespace Volo.Abp.Caching TCacheItem value, DistributedCacheEntryOptions options = null, bool? hideErrors = null, + bool considerUow = false, CancellationToken token = default) { - hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; - - try + async Task SetRealCache() { - await Cache.SetAsync( - NormalizeKey(key), - Serializer.Serialize(value), - options ?? DefaultCacheOptions, - CancellationTokenProvider.FallbackToProvider(token) - ); - } - catch (Exception ex) - { - if (hideErrors == true) + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try { - await HandleExceptionAsync(ex); - return; + await Cache.SetAsync( + NormalizeKey(key), + Serializer.Serialize(value), + options ?? DefaultCacheOptions, + CancellationTokenProvider.FallbackToProvider(token) + ); } + catch (Exception ex) + { + if (hideErrors == true) + { + await HandleExceptionAsync(ex); + return; + } - throw; + throw; + } } + + if (ShouldConsiderUow(considerUow)) + { + var uowCache = GetUnitOfWorkCache(); + if (uowCache.TryGetValue(key, out _)) + { + uowCache[key].SetValue(value); + } + else + { + uowCache.Add(key, new UnitOfWorkCacheItem(value)); + } + + // ReSharper disable once PossibleNullReferenceException + UnitOfWorkManager.Current.OnCompleted(SetRealCache); + } + else + { + await SetRealCache(); + } } public void SetMany( IEnumerable> items, DistributedCacheEntryOptions options = null, - bool? hideErrors = null) + bool? hideErrors = null, + bool considerUow = false) { var itemsArray = items.ToArray(); @@ -502,40 +670,73 @@ namespace Volo.Abp.Caching SetManyFallback( itemsArray, options, - hideErrors + hideErrors, + considerUow ); - + return; } - - hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; - try + void SetRealCache() { - cacheSupportsMultipleItems.SetMany( - ToRawCacheItems(itemsArray), - options ?? DefaultCacheOptions - ); + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + cacheSupportsMultipleItems.SetMany( + ToRawCacheItems(itemsArray), + options ?? DefaultCacheOptions + ); + } + catch (Exception ex) + { + if (hideErrors == true) + { + HandleException(ex); + return; + } + + throw; + } } - catch (Exception ex) + + if (ShouldConsiderUow(considerUow)) { - if (hideErrors == true) + var uowCache = GetUnitOfWorkCache(); + + foreach (var pair in itemsArray) { - HandleException(ex); - return; + if (uowCache.TryGetValue(pair.Key, out _)) + { + uowCache[pair.Key].SetValue(pair.Value); + } + else + { + uowCache.Add(pair.Key, new UnitOfWorkCacheItem(pair.Value)); + } } - throw; + // ReSharper disable once PossibleNullReferenceException + UnitOfWorkManager.Current.OnCompleted(() => + { + SetRealCache(); + return Task.CompletedTask; + }); + } + else + { + SetRealCache(); } } - + protected virtual void SetManyFallback( KeyValuePair[] items, DistributedCacheEntryOptions options = null, - bool? hideErrors = null) + bool? hideErrors = null, + bool considerUow = false) { hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; - + try { foreach (var item in items) @@ -543,8 +744,9 @@ namespace Volo.Abp.Caching Set( item.Key, item.Value, - options: options, - hideErrors: false + options, + false, + considerUow ); } } @@ -564,6 +766,7 @@ namespace Volo.Abp.Caching IEnumerable> items, DistributedCacheEntryOptions options = null, bool? hideErrors = null, + bool considerUow = false, CancellationToken token = default) { var itemsArray = items.ToArray(); @@ -575,38 +778,67 @@ namespace Volo.Abp.Caching itemsArray, options, hideErrors, + considerUow, token ); - + return; } - - hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; - try + async Task SetRealCache() { - await cacheSupportsMultipleItems.SetManyAsync( - ToRawCacheItems(itemsArray), - options ?? DefaultCacheOptions, - CancellationTokenProvider.FallbackToProvider(token) - ); + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + await cacheSupportsMultipleItems.SetManyAsync( + ToRawCacheItems(itemsArray), + options ?? DefaultCacheOptions, + CancellationTokenProvider.FallbackToProvider(token) + ); + } + catch (Exception ex) + { + if (hideErrors == true) + { + await HandleExceptionAsync(ex); + return; + } + + throw; + } } - catch (Exception ex) + + if (ShouldConsiderUow(considerUow)) { - if (hideErrors == true) + var uowCache = GetUnitOfWorkCache(); + + foreach (var pair in itemsArray) { - await HandleExceptionAsync(ex); - return; + if (uowCache.TryGetValue(pair.Key, out _)) + { + uowCache[pair.Key].SetValue(pair.Value); + } + else + { + uowCache.Add(pair.Key, new UnitOfWorkCacheItem(pair.Value)); + } } - throw; + // ReSharper disable once PossibleNullReferenceException + UnitOfWorkManager.Current.OnCompleted(SetRealCache); + } + else + { + await SetRealCache(); } } - + protected virtual async Task SetManyFallbackAsync( KeyValuePair[] items, DistributedCacheEntryOptions options = null, bool? hideErrors = null, + bool considerUow = false, CancellationToken token = default) { hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; @@ -618,8 +850,9 @@ namespace Volo.Abp.Caching await SetAsync( item.Key, item.Value, - options: options, - hideErrors: false, + options, + false, + considerUow, token: token ); } @@ -636,9 +869,14 @@ namespace Volo.Abp.Caching } } + /// + /// Refreshes the cache value of the given key, and resets its sliding expiration timeout. + /// + /// The key of cached item to be retrieved from the cache. + /// Indicates to throw or hide the exceptions for the distributed cache. public virtual void Refresh( - TCacheKey key, bool? - hideErrors = null) + TCacheKey key, + bool? hideErrors = null) { hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; @@ -658,6 +896,13 @@ namespace Volo.Abp.Caching } } + /// + /// Refreshes the cache value of the given key, and resets its sliding expiration timeout. + /// + /// The key of cached item to be retrieved from the cache. + /// Indicates to throw or hide the exceptions for the distributed cache. + /// The for the task. + /// The indicating that the operation is asynchronous. public virtual async Task RefreshAsync( TCacheKey key, bool? hideErrors = null, @@ -681,48 +926,106 @@ namespace Volo.Abp.Caching } } + /// + /// Removes the cache item for given key from cache. + /// + /// The key of cached item to be retrieved from the cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. + /// Indicates to throw or hide the exceptions for the distributed cache. public virtual void Remove( TCacheKey key, - bool? hideErrors = null) + bool? hideErrors = null, + bool considerUow = false) { - hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; - - try + void RemoveRealCache() { - Cache.Remove(NormalizeKey(key)); + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + Cache.Remove(NormalizeKey(key)); + } + catch (Exception ex) + { + if (hideErrors == true) + { + HandleException(ex); + return; + } + + throw; + } } - catch (Exception ex) + + if (ShouldConsiderUow(considerUow)) { - if (hideErrors == true) + var uowCache = GetUnitOfWorkCache(); + if (uowCache.TryGetValue(key, out _)) { - HandleException(ex); - return; + uowCache[key].RemoveValue(); } - throw; + // ReSharper disable once PossibleNullReferenceException + UnitOfWorkManager.Current.OnCompleted(() => + { + RemoveRealCache(); + return Task.CompletedTask; + }); + } + else + { + RemoveRealCache(); } } + /// + /// Removes the cache item for given key from cache. + /// + /// The key of cached item to be retrieved from the cache. + /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. + /// The for the task. + /// The indicating that the operation is asynchronous. public virtual async Task RemoveAsync( TCacheKey key, bool? hideErrors = null, + bool considerUow = false, CancellationToken token = default) { - hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; - - try + async Task RemoveRealCache() { - await Cache.RemoveAsync(NormalizeKey(key), CancellationTokenProvider.FallbackToProvider(token)); + hideErrors = hideErrors ?? _distributedCacheOption.HideErrors; + + try + { + await Cache.RemoveAsync(NormalizeKey(key), CancellationTokenProvider.FallbackToProvider(token)); + } + catch (Exception ex) + { + if (hideErrors == true) + { + await HandleExceptionAsync(ex); + return; + } + + throw; + } } - catch (Exception ex) + + if (ShouldConsiderUow(considerUow)) { - if (hideErrors == true) + var uowCache = GetUnitOfWorkCache(); + if (uowCache.TryGetValue(key, out _)) { - await HandleExceptionAsync(ex); - return; + uowCache[key].RemoveValue(); } - throw; + // ReSharper disable once PossibleNullReferenceException + UnitOfWorkManager.Current.OnCompleted(RemoveRealCache); + } + else + { + await RemoveRealCache(); } } @@ -730,7 +1033,7 @@ namespace Volo.Abp.Caching { AsyncHelper.RunSync(() => HandleExceptionAsync(ex)); } - + protected virtual async Task HandleExceptionAsync(Exception ex) { Logger.LogException(ex, LogLevel.Warning); @@ -742,7 +1045,7 @@ namespace Volo.Abp.Caching .NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning)); } } - + protected virtual KeyValuePair[] ToCacheItems(byte[][] itemBytes, TCacheKey[] itemKeys) { if (itemBytes.Length != itemKeys.Length) @@ -764,7 +1067,7 @@ namespace Volo.Abp.Caching return result.ToArray(); } - + [CanBeNull] protected virtual TCacheItem ToCacheItem([CanBeNull] byte[] bytes) { @@ -786,12 +1089,33 @@ namespace Volo.Abp.Caching ) ).ToArray(); } - + private static KeyValuePair[] ToCacheItemsWithDefaultValues(TCacheKey[] keys) { return keys .Select(key => new KeyValuePair(key, default)) .ToArray(); } + + protected virtual bool ShouldConsiderUow(bool considerUow) + { + return considerUow && UnitOfWorkManager.Current != null; + } + + protected virtual string GetUnitOfWorkCacheKey() + { + return UowCacheName + CacheName; + } + + protected virtual Dictionary> GetUnitOfWorkCache() + { + if (UnitOfWorkManager.Current == null) + { + throw new AbpException($"There is no active UOW."); + } + + return UnitOfWorkManager.Current.GetOrAddItem(GetUnitOfWorkCacheKey(), + key => new Dictionary>()); + } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs index 80fe124785..2f74fd678b 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs @@ -30,42 +30,48 @@ namespace Volo.Abp.Caching /// /// The key of cached item to be retrieved from the cache. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. /// The cache item, or null. TCacheItem Get( TCacheKey key, - bool? hideErrors = null + bool? hideErrors = null, + bool considerUow = false ); - + /// /// Gets multiple cache items with the given keys. /// /// The returned list contains exactly the same count of items specified in the given keys. /// An item in the return list can not be null, but an item in the list has null value - /// if the related key not found in the cache. + /// if the related key not found in the cache. /// /// The keys of cached items to be retrieved from the cache. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. /// List of cache items. KeyValuePair[] GetMany( IEnumerable keys, - bool? hideErrors = null + bool? hideErrors = null, + bool considerUow = false ); - + /// /// Gets multiple cache items with the given keys. /// /// The returned list contains exactly the same count of items specified in the given keys. /// An item in the return list can not be null, but an item in the list has null value /// if the related key not found in the cache. - /// + /// /// /// The keys of cached items to be retrieved from the cache. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. /// /// The for the task. /// List of cache items. Task[]> GetManyAsync( IEnumerable keys, bool? hideErrors = null, + bool considerUow = false, CancellationToken token = default ); @@ -74,11 +80,13 @@ namespace Volo.Abp.Caching /// /// The key of cached item to be retrieved from the cache. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. /// The for the task. /// The cache item, or null. Task GetAsync( [NotNull] TCacheKey key, bool? hideErrors = null, + bool considerUow = false, CancellationToken token = default ); @@ -90,12 +98,14 @@ namespace Volo.Abp.Caching /// The factory delegate is used to provide the cache item when no cache item is found for the given . /// The cache options for the factory delegate. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. /// The cache item. TCacheItem GetOrAdd( TCacheKey key, Func factory, Func optionsFactory = null, - bool? hideErrors = null + bool? hideErrors = null, + bool considerUow = false ); /// @@ -106,6 +116,7 @@ namespace Volo.Abp.Caching /// The factory delegate is used to provide the cache item when no cache item is found for the given . /// The cache options for the factory delegate. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. /// The for the task. /// The cache item. Task GetOrAddAsync( @@ -113,6 +124,7 @@ namespace Volo.Abp.Caching Func> factory, Func optionsFactory = null, bool? hideErrors = null, + bool considerUow = false, CancellationToken token = default ); @@ -123,11 +135,13 @@ namespace Volo.Abp.Caching /// The cache item value to set in the cache. /// The cache options for the value. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. void Set( TCacheKey key, TCacheItem value, DistributedCacheEntryOptions options = null, - bool? hideErrors = null + bool? hideErrors = null, + bool considerUow = false ); /// @@ -137,6 +151,7 @@ namespace Volo.Abp.Caching /// The cache item value to set in the cache. /// The cache options for the value. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. /// The for the task. /// The indicating that the operation is asynchronous. Task SetAsync( @@ -144,6 +159,7 @@ namespace Volo.Abp.Caching [NotNull] TCacheItem value, [CanBeNull] DistributedCacheEntryOptions options = null, bool? hideErrors = null, + bool considerUow = false, CancellationToken token = default ); @@ -154,12 +170,14 @@ namespace Volo.Abp.Caching /// Items to set on the cache /// The cache options for the value. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. void SetMany( IEnumerable> items, DistributedCacheEntryOptions options = null, - bool? hideErrors = null + bool? hideErrors = null, + bool considerUow = false ); - + /// /// Sets multiple cache items. /// Based on the implementation, this can be more efficient than setting multiple items individually. @@ -167,12 +185,14 @@ namespace Volo.Abp.Caching /// Items to set on the cache /// The cache options for the value. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. /// The for the task. /// The indicating that the operation is asynchronous. Task SetManyAsync( IEnumerable> items, DistributedCacheEntryOptions options = null, bool? hideErrors = null, + bool considerUow = false, CancellationToken token = default ); @@ -204,9 +224,11 @@ namespace Volo.Abp.Caching /// /// The key of cached item to be retrieved from the cache. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. void Remove( TCacheKey key, - bool? hideErrors = null + bool? hideErrors = null, + bool considerUow = false ); /// @@ -214,11 +236,13 @@ namespace Volo.Abp.Caching /// /// The key of cached item to be retrieved from the cache. /// Indicates to throw or hide the exceptions for the distributed cache. + /// This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache. /// The for the task. /// The indicating that the operation is asynchronous. Task RemoveAsync( TCacheKey key, bool? hideErrors = null, + bool considerUow = false, CancellationToken token = default ); } diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/UnitOfWorkCacheItem.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/UnitOfWorkCacheItem.cs new file mode 100644 index 0000000000..c37475d022 --- /dev/null +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/UnitOfWorkCacheItem.cs @@ -0,0 +1,43 @@ +using System; + +namespace Volo.Abp.Caching +{ + [Serializable] + public class UnitOfWorkCacheItem + where TValue : class + { + public bool IsRemoved { get; set; } + + public TValue Value { get; set; } + + public UnitOfWorkCacheItem() + { + + } + + public UnitOfWorkCacheItem(TValue value) + { + Value = value; + } + + public UnitOfWorkCacheItem(TValue value, bool isRemoved) + { + Value = value; + IsRemoved = isRemoved; + } + + public UnitOfWorkCacheItem SetValue(TValue value) + { + Value = value; + IsRemoved = false; + return this; + } + + public UnitOfWorkCacheItem RemoveValue() + { + Value = null; + IsRemoved = true; + return this; + } + } +} diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/UnitOfWorkCacheItemExtensions.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/UnitOfWorkCacheItemExtensions.cs new file mode 100644 index 0000000000..46757f80cf --- /dev/null +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/UnitOfWorkCacheItemExtensions.cs @@ -0,0 +1,11 @@ +namespace Volo.Abp.Caching +{ + public static class UnitOfWorkCacheItemExtensions + { + public static TValue GetUnRemovedValueOrNull(this UnitOfWorkCacheItem item) + where TValue : class + { + return item != null && !item.IsRemoved ? item.Value : null; + } + } +} diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyCrudAppService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyCrudAppService.cs index 11550c456f..91879f030e 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyCrudAppService.cs +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyCrudAppService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Volo.Abp.Application.Dtos; using Volo.Abp.Domain.Entities; @@ -51,6 +52,11 @@ namespace Volo.Abp.Application.Services } + protected override Task MapToGetListOutputDtoAsync(TEntity entity) + { + return MapToGetOutputDtoAsync(entity); + } + protected override TEntityDto MapToGetListOutputDto(TEntity entity) { return MapToGetOutputDto(entity); @@ -80,13 +86,13 @@ namespace Volo.Abp.Application.Services { await CheckCreatePolicyAsync(); - var entity = MapToEntity(input); + var entity = await MapToEntityAsync(input); TryToSetTenantId(entity); await Repository.InsertAsync(entity, autoSave: true); - return MapToGetOutputDto(entity); + return await MapToGetOutputDtoAsync(entity); } public virtual async Task UpdateAsync(TKey id, TUpdateInput input) @@ -95,10 +101,10 @@ namespace Volo.Abp.Application.Services var entity = await GetEntityByIdAsync(id); //TODO: Check if input has id different than given id and normalize if it's default value, throw ex otherwise - MapToEntity(input, entity); + await MapToEntityAsync(input, entity); await Repository.UpdateAsync(entity, autoSave: true); - return MapToGetOutputDto(entity); + return await MapToGetOutputDtoAsync(entity); } public virtual async Task DeleteAsync(TKey id) @@ -125,6 +131,17 @@ namespace Volo.Abp.Application.Services await CheckPolicyAsync(DeletePolicyName); } + /// + /// Maps to to create a new entity. + /// It uses by default. + /// It can be overriden for custom mapping. + /// Overriding this has higher priority than overriding the + /// + protected virtual Task MapToEntityAsync(TCreateInput createInput) + { + return Task.FromResult(MapToEntity(createInput)); + } + /// /// Maps to to create a new entity. /// It uses by default. @@ -153,6 +170,18 @@ namespace Volo.Abp.Application.Services } } + /// + /// Maps to to update the entity. + /// It uses by default. + /// It can be overriden for custom mapping. + /// Overriding this has higher priority than overriding the + /// + protected virtual Task MapToEntityAsync(TUpdateInput updateInput, TEntity entity) + { + MapToEntity(updateInput, entity); + return Task.CompletedTask; + } + /// /// Maps to to update the entity. /// It uses by default. @@ -190,4 +219,4 @@ namespace Volo.Abp.Application.Services return entity.GetType().GetProperty(nameof(IMultiTenant.TenantId)) != null; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs index 9085434211..b9ffe3359b 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Dynamic.Core; using System.Threading.Tasks; @@ -6,6 +7,7 @@ using Volo.Abp.Application.Dtos; using Volo.Abp.Auditing; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories; +using Volo.Abp.ObjectMapping; namespace Volo.Abp.Application.Services { @@ -52,7 +54,8 @@ namespace Volo.Abp.Application.Services await CheckGetPolicyAsync(); var entity = await GetEntityByIdAsync(id); - return MapToGetOutputDto(entity); + + return await MapToGetOutputDtoAsync(entity); } public virtual async Task> GetListAsync(TGetListInput input) @@ -67,10 +70,11 @@ namespace Volo.Abp.Application.Services query = ApplyPaging(query, input); var entities = await AsyncExecuter.ToListAsync(query); + var entityDtos = await MapToGetListOutputDtosAsync(entities); return new PagedResultDto( totalCount, - entities.Select(MapToGetListOutputDto).ToList() + entityDtos ); } @@ -118,9 +122,9 @@ namespace Volo.Abp.Application.Services /// The query. protected virtual IQueryable ApplyDefaultSorting(IQueryable query) { - if (typeof(TEntity).IsAssignableTo()) + if (typeof(TEntity).IsAssignableTo()) { - return query.OrderByDescending(e => ((ICreationAuditedObject)e).CreationTime); + return query.OrderByDescending(e => ((IHasCreationTime)e).CreationTime); } throw new AbpException("No sorting specified but this query requires sorting. Override the ApplyDefaultSorting method for your application service derived from AbstractKeyReadOnlyAppService!"); @@ -161,6 +165,17 @@ namespace Volo.Abp.Application.Services return ReadOnlyRepository; } + /// + /// Maps to . + /// It internally calls the by default. + /// It can be overriden for custom mapping. + /// Overriding this has higher priority than overriding the + /// + protected virtual Task MapToGetOutputDtoAsync(TEntity entity) + { + return Task.FromResult(MapToGetOutputDto(entity)); + } + /// /// Maps to . /// It uses by default. @@ -171,6 +186,33 @@ namespace Volo.Abp.Application.Services return ObjectMapper.Map(entity); } + /// + /// Maps a list of to objects. + /// It uses method for each item in the list. + /// + protected virtual async Task> MapToGetListOutputDtosAsync(List entities) + { + var dtos = new List(); + + foreach (var entity in entities) + { + dtos.Add(await MapToGetListOutputDtoAsync(entity)); + } + + return dtos; + } + + /// + /// Maps to . + /// It internally calls the by default. + /// It can be overriden for custom mapping. + /// Overriding this has higher priority than overriding the + /// + protected virtual Task MapToGetListOutputDtoAsync(TEntity entity) + { + return Task.FromResult(MapToGetListOutputDto(entity)); + } + /// /// Maps to . /// It uses by default. diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs index 7832e8a4ab..db66881fb7 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs @@ -55,6 +55,11 @@ namespace Volo.Abp.Application.Services } + protected override Task MapToGetListOutputDtoAsync(TEntity entity) + { + return base.MapToGetOutputDtoAsync(entity); + } + protected override TEntityDto MapToGetListOutputDto(TEntity entity) { return MapToGetOutputDto(entity); @@ -97,9 +102,9 @@ namespace Volo.Abp.Application.Services protected override IQueryable ApplyDefaultSorting(IQueryable query) { - if (typeof(TEntity).IsAssignableTo()) + if (typeof(TEntity).IsAssignableTo()) { - return query.OrderByDescending(e => ((ICreationAuditedObject)e).CreationTime); + return query.OrderByDescending(e => ((IHasCreationTime)e).CreationTime); } else { diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index ed7d37db20..e8c7fc6c5b 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -9,6 +9,7 @@ using RabbitMQ.Client; using RabbitMQ.Client.Events; using Volo.Abp.DependencyInjection; using Volo.Abp.EventBus.Distributed; +using Volo.Abp.MultiTenancy; using Volo.Abp.RabbitMQ; using Volo.Abp.Threading; @@ -26,7 +27,7 @@ namespace Volo.Abp.EventBus.RabbitMq protected AbpDistributedEventBusOptions AbpDistributedEventBusOptions { get; } protected IConnectionPool ConnectionPool { get; } protected IRabbitMqSerializer Serializer { get; } - + //TODO: Accessing to the List may not be thread-safe! protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } @@ -37,17 +38,18 @@ namespace Volo.Abp.EventBus.RabbitMq IOptions options, IConnectionPool connectionPool, IRabbitMqSerializer serializer, - IServiceScopeFactory serviceScopeFactory, + IServiceScopeFactory serviceScopeFactory, IOptions distributedEventBusOptions, - IRabbitMqMessageConsumerFactory messageConsumerFactory) - : base(serviceScopeFactory) + IRabbitMqMessageConsumerFactory messageConsumerFactory, + ICurrentTenant currentTenant) + : base(serviceScopeFactory, currentTenant) { ConnectionPool = connectionPool; Serializer = serializer; MessageConsumerFactory = messageConsumerFactory; AbpDistributedEventBusOptions = distributedEventBusOptions.Value; AbpRabbitMqEventBusOptions = options.Value; - + HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); } @@ -178,7 +180,7 @@ namespace Volo.Abp.EventBus.RabbitMq "direct", durable: true ); - + var properties = channel.CreateBasicProperties(); properties.DeliveryMode = RabbitMqConsts.DeliveryModes.Persistent; diff --git a/framework/src/Volo.Abp.EventBus/Volo.Abp.EventBus.csproj b/framework/src/Volo.Abp.EventBus/Volo.Abp.EventBus.csproj index 566767e111..a1799c6673 100644 --- a/framework/src/Volo.Abp.EventBus/Volo.Abp.EventBus.csproj +++ b/framework/src/Volo.Abp.EventBus/Volo.Abp.EventBus.csproj @@ -16,6 +16,7 @@ + diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/AbpEventBusModule.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/AbpEventBusModule.cs index c5a5022f77..81c3393a50 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/AbpEventBusModule.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/AbpEventBusModule.cs @@ -4,10 +4,12 @@ using System.Collections.Generic; using Volo.Abp.EventBus.Distributed; using Volo.Abp.EventBus.Local; using Volo.Abp.Modularity; +using Volo.Abp.MultiTenancy; using Volo.Abp.Reflection; namespace Volo.Abp.EventBus { + [DependsOn(typeof(AbpMultiTenancyModule))] public class AbpEventBusModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs index a7883950d1..80d4134db2 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Collections; using Volo.Abp.EventBus.Distributed; +using Volo.Abp.MultiTenancy; using Volo.Abp.Reflection; namespace Volo.Abp.EventBus @@ -16,9 +17,12 @@ namespace Volo.Abp.EventBus { protected IServiceScopeFactory ServiceScopeFactory { get; } - protected EventBusBase(IServiceScopeFactory serviceScopeFactory) + protected ICurrentTenant CurrentTenant { get; } + + protected EventBusBase(IServiceScopeFactory serviceScopeFactory, ICurrentTenant currentTenant) { ServiceScopeFactory = serviceScopeFactory; + CurrentTenant = currentTenant; } /// @@ -162,31 +166,34 @@ namespace Volo.Abp.EventBus { var handlerType = eventHandlerWrapper.EventHandler.GetType(); - if (ReflectionHelper.IsAssignableToGenericType(handlerType, typeof(ILocalEventHandler<>))) - { - var method = typeof(ILocalEventHandler<>) - .MakeGenericType(eventType) - .GetMethod( - nameof(ILocalEventHandler.HandleEventAsync), - new[] { eventType } - ); - - await ((Task)method.Invoke(eventHandlerWrapper.EventHandler, new[] { eventData })); - } - else if (ReflectionHelper.IsAssignableToGenericType(handlerType, typeof(IDistributedEventHandler<>))) - { - var method = typeof(IDistributedEventHandler<>) - .MakeGenericType(eventType) - .GetMethod( - nameof(IDistributedEventHandler.HandleEventAsync), - new[] { eventType } - ); - - await ((Task)method.Invoke(eventHandlerWrapper.EventHandler, new[] { eventData })); - } - else + using (CurrentTenant.Change(GetEventDataTenantId(eventData))) { - throw new AbpException("The object instance is not an event handler. Object type: " + handlerType.AssemblyQualifiedName); + if (ReflectionHelper.IsAssignableToGenericType(handlerType, typeof(ILocalEventHandler<>))) + { + var method = typeof(ILocalEventHandler<>) + .MakeGenericType(eventType) + .GetMethod( + nameof(ILocalEventHandler.HandleEventAsync), + new[] { eventType } + ); + + await ((Task)method.Invoke(eventHandlerWrapper.EventHandler, new[] { eventData })); + } + else if (ReflectionHelper.IsAssignableToGenericType(handlerType, typeof(IDistributedEventHandler<>))) + { + var method = typeof(IDistributedEventHandler<>) + .MakeGenericType(eventType) + .GetMethod( + nameof(IDistributedEventHandler.HandleEventAsync), + new[] { eventType } + ); + + await ((Task)method.Invoke(eventHandlerWrapper.EventHandler, new[] { eventData })); + } + else + { + throw new AbpException("The object instance is not an event handler. Object type: " + handlerType.AssemblyQualifiedName); + } } } catch (TargetInvocationException ex) @@ -200,6 +207,16 @@ namespace Volo.Abp.EventBus } } + protected virtual Guid? GetEventDataTenantId(object eventData) + { + return eventData switch + { + IMultiTenant multiTenantEventData => multiTenantEventData.TenantId, + IEventDataMayHaveTenantId eventDataMayHaveTenantId when eventDataMayHaveTenantId.IsMultiTenant(out var tenantId) => tenantId, + _ => CurrentTenant.Id + }; + } + protected class EventTypeWithEventHandlerFactories { public Type EventType { get; } @@ -246,4 +263,4 @@ namespace Volo.Abp.EventBus } } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs index 7db5c18f01..8c7bef6f2d 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; using Volo.Abp.Threading; namespace Volo.Abp.EventBus.Local @@ -29,8 +30,9 @@ namespace Volo.Abp.EventBus.Local public LocalEventBus( IOptions options, - IServiceScopeFactory serviceScopeFactory) - : base(serviceScopeFactory) + IServiceScopeFactory serviceScopeFactory, + ICurrentTenant currentTenant) + : base(serviceScopeFactory, currentTenant) { Options = options.Value; Logger = NullLogger.Instance; @@ -166,4 +168,4 @@ namespace Volo.Abp.EventBus.Local return false; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs index b42b745fb4..b4158229fe 100644 --- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Threading; @@ -316,4 +316,4 @@ namespace Volo.Abp.Uow return $"[UnitOfWork {Id}]"; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkExtensions.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkExtensions.cs index 5c01f54a99..c183a70390 100644 --- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkExtensions.cs +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkExtensions.cs @@ -1,4 +1,7 @@ -using JetBrains.Annotations; +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; namespace Volo.Abp.Uow { @@ -10,5 +13,43 @@ namespace Volo.Abp.Uow return unitOfWork.IsReserved && unitOfWork.ReservationName == reservationName; } + + public static void AddItem([NotNull] this IUnitOfWork unitOfWork, string key, TValue value) + where TValue : class + { + Check.NotNull(unitOfWork, nameof(unitOfWork)); + + if (!unitOfWork.Items.ContainsKey(key)) + { + unitOfWork.Items[key] = value; + } + else + { + unitOfWork.Items.Add(key, value); + } + } + + public static TValue GetItemOrDefault([NotNull] this IUnitOfWork unitOfWork, string key) + where TValue : class + { + Check.NotNull(unitOfWork, nameof(unitOfWork)); + + return unitOfWork.Items.FirstOrDefault(x => x.Key == key).As(); + } + + public static TValue GetOrAddItem([NotNull] this IUnitOfWork unitOfWork, string key, Func factory) + where TValue : class + { + Check.NotNull(unitOfWork, nameof(unitOfWork)); + + return unitOfWork.Items.GetOrAdd(key, factory).As(); + } + + public static void RemoveItem([NotNull] this IUnitOfWork unitOfWork, string key) + { + Check.NotNull(unitOfWork, nameof(unitOfWork)); + + unitOfWork.Items.RemoveAll(x => x.Key == key); + } } -} \ No newline at end of file +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/FormElements.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/FormElements.cshtml index 889e798c8a..6fa9cabb37 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/FormElements.cshtml +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/FormElements.cshtml @@ -108,7 +108,7 @@ public class FormElementsModel : PageModel { public SampleModel MyModel { get; set; } - + public List<SelectListItem> CityList { get; set; } = new List<SelectListItem> { new SelectListItem { Value = "NY", Text = "New York"}, @@ -260,7 +260,7 @@ public class SampleModel { public string SampleInput0 { get; set; } - + public string SampleInput1 { get; set; } public string SampleInput2 { get; set; } @@ -354,7 +354,7 @@ public class FormElementsModel : PageModel { public SampleModel MyModel { get; set; } - + public List"SelectListItem" CityList { get; set; } = new List"SelectListItem" { new SelectListItem { Value = "NY", Text = "New York"}, @@ -458,6 +458,51 @@ <option value="3">Coupe</option> </select> </div> + + + + + + +

Suppress Label Generation

+ +
+
+ +
+
+ + +

+ public class FormElementsModel : PageModel
+ {
+     public SampleModel MyModel { get; set; }
+
+     public void OnGet()
+     {
+         MyModel = new SampleModel();
+     }
+
+     public class SampleModel
+     {
+         [Required]
+         public string Name { get; set; }
+     }
+ }
+
+
+
+ +

+<abp-input asp-for="@@Model.MyModel.Name" suppress-label="true"/>
+                
+
+ +

+<div class="form-group">
+    <input type="text" id="MyModel_Name" name="MyModel.Name" value="" class="form-control ">
+    <span class="text-danger field-validation-valid" data-valmsg-for="MyModel.Name" data-valmsg-replace="true"></span>
+</div>
 
diff --git a/framework/test/Volo.Abp.BlobStoring.Aliyun.Tests/Volo/Abp/BlobStoring/Aliyun/AbpBlobStoringAliyunTestModule.cs b/framework/test/Volo.Abp.BlobStoring.Aliyun.Tests/Volo/Abp/BlobStoring/Aliyun/AbpBlobStoringAliyunTestModule.cs index 1635e3390f..b08737eba5 100644 --- a/framework/test/Volo.Abp.BlobStoring.Aliyun.Tests/Volo/Abp/BlobStoring/Aliyun/AbpBlobStoringAliyunTestModule.cs +++ b/framework/test/Volo.Abp.BlobStoring.Aliyun.Tests/Volo/Abp/BlobStoring/Aliyun/AbpBlobStoringAliyunTestModule.cs @@ -50,14 +50,16 @@ namespace Volo.Abp.BlobStoring.Aliyun aliyun.AccessKeySecret = accessKeySecret; aliyun.Endpoint = endpoint; //STS + aliyun.UseSecurityTokenService = true; aliyun.RegionId = regionId; aliyun.RoleArn = roleArn; aliyun.RoleSessionName = Guid.NewGuid().ToString("N"); - aliyun.DurationSeconds = 0; + aliyun.DurationSeconds = 900; aliyun.Policy = String.Empty; //Other aliyun.CreateContainerIfNotExists = true; aliyun.ContainerName = _randomContainerName; + aliyun.TemporaryCredentialsCacheKey = "297A96094D7048DBB2C28C3FDB20839A"; _configuration = aliyun; }); }); diff --git a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/AbpCachingTestModule.cs b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/AbpCachingTestModule.cs index be003431f2..9da6725fa5 100644 --- a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/AbpCachingTestModule.cs +++ b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/AbpCachingTestModule.cs @@ -1,5 +1,7 @@ using Microsoft.Extensions.Caching.Distributed; using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Volo.Abp.Modularity; namespace Volo.Abp.Caching @@ -26,6 +28,8 @@ namespace Volo.Abp.Caching option.GlobalCacheEntryOptions.SetSlidingExpiration(TimeSpan.FromMinutes(20)); }); + + context.Services.Replace(ServiceDescriptor.Singleton()); } } -} \ No newline at end of file +} diff --git a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_Tests.cs b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_Tests.cs index 533362c851..32876c08fe 100644 --- a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_Tests.cs +++ b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_Tests.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Shouldly; using Volo.Abp.Testing; +using Volo.Abp.Uow; using Xunit; namespace Volo.Abp.Caching @@ -30,7 +32,7 @@ namespace Volo.Abp.Caching cacheItem.ShouldNotBeNull(); cacheItem.Name.ShouldBe(personName); - //Remove + //Remove await personCache.RemoveAsync(cacheKey); //Get (not exists since removed) @@ -111,7 +113,7 @@ namespace Volo.Abp.Caching cacheItem1.ShouldNotBeNull(); cacheItem1.Name.ShouldBe(personName); - //Remove + //Remove await personCache.RemoveAsync(cacheKey); @@ -145,7 +147,7 @@ namespace Volo.Abp.Caching cacheItem.ShouldNotBeNull(); cacheItem.Name.ShouldBe(personName); - //Remove + //Remove await personCache.RemoveAsync(cacheKey); //Get (not exists since removed) @@ -180,10 +182,10 @@ namespace Volo.Abp.Caching factoryExecuted = false; cacheItem = await personCache.GetOrAddAsync(cacheKey, - async () => + () => { factoryExecuted = true; - return new PersonCacheItem(personName); + return Task.FromResult(new PersonCacheItem(personName)); }); factoryExecuted.ShouldBeFalse(); @@ -227,7 +229,7 @@ namespace Volo.Abp.Caching cacheItem1.ShouldNotBeNull(); cacheItem1.Name.ShouldBe(personName); - //Remove + //Remove await personCache.RemoveAsync(cacheKey); @@ -261,7 +263,7 @@ namespace Volo.Abp.Caching cacheItem.ShouldNotBeNull(); cacheItem.Name.ShouldBe(personName); - //Remove + //Remove await personCache.RemoveAsync(cacheKey); //Get (not exists since removed) @@ -299,7 +301,7 @@ namespace Volo.Abp.Caching cacheItem2.ShouldNotBeNull(); cacheItem2.Name.ShouldBe(personName); - //Remove + //Remove await personCache.RemoveAsync(cache1Key); await personCache.RemoveAsync(cache2Key); @@ -310,6 +312,377 @@ namespace Volo.Abp.Caching cacheItem2.ShouldBeNull(); } + [Fact] + public async Task Cache_Should_Only_Available_In_Uow_For_GetAsync() + { + const string key = "testkey"; + + using (var uow = GetRequiredService().Begin()) + { + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetAsync(key, considerUow: true); + cacheValue.ShouldBeNull(); + + await personCache.SetAsync(key, new PersonCacheItem("john"), considerUow: true); + + cacheValue = await personCache.GetAsync(key, considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + + uow.OnCompleted(async () => + { + cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + }); + + await uow.CompleteAsync(); + } + } + + [Fact] + public async Task Cache_Should_Rollback_With_Uow_For_GetAsync() + { + const string key = "testkey"; + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + + using (var uow = GetRequiredService().Begin()) + { + cacheValue = await personCache.GetAsync(key, considerUow: true); + cacheValue.ShouldBeNull(); + + await personCache.SetAsync(key, new PersonCacheItem("john"), considerUow: true); + + cacheValue = await personCache.GetAsync(key, considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + } + + cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + } + + [Fact] + public async Task Cache_Should_Only_Available_In_Uow_For_GetOrAddAsync() + { + const string key = "testkey"; + + using (var uow = GetRequiredService().Begin()) + { + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetOrAddAsync(key, () => Task.FromResult(new PersonCacheItem("john")), considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + cacheValue = await personCache.GetAsync(key, considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + + uow.OnCompleted(async () => + { + cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + }); + + await uow.CompleteAsync(); + } + } + + [Fact] + public async Task Cache_Should_Rollback_With_Uow_For_GetOrAddAsync() + { + const string key = "testkey"; + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + + using (var uow = GetRequiredService().Begin()) + { + cacheValue = await personCache.GetOrAddAsync(key, () => Task.FromResult(new PersonCacheItem("john")), considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + cacheValue = await personCache.GetAsync(key, considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + } + + cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + } + + [Fact] + public async Task Cache_Should_Only_Available_In_Uow_For_SetAsync() + { + const string key = "testkey"; + + using (var uow = GetRequiredService().Begin()) + { + var personCache = GetRequiredService>(); + + await personCache.SetAsync(key, new PersonCacheItem("john"), considerUow: true); + + var cacheValue = await personCache.GetAsync(key, considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + + uow.OnCompleted(async () => + { + cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + }); + + await uow.CompleteAsync(); + } + } + + [Fact] + public async Task Cache_Should_Rollback_With_Uow_For_SetAsync() + { + const string key = "testkey"; + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + + using (var uow = GetRequiredService().Begin()) + { + await personCache.SetAsync(key, new PersonCacheItem("john"), considerUow: true); + + cacheValue = await personCache.GetAsync(key, considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + } + + cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + } + + [Fact] + public async Task Cache_Should_Only_Available_In_Uow_For_RemoveAsync() + { + const string key = "testkey"; + + using (var uow = GetRequiredService().Begin()) + { + var personCache = GetRequiredService>(); + + await personCache.SetAsync(key, new PersonCacheItem("john"), considerUow: true); + + var cacheValue = await personCache.GetAsync(key, considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + await personCache.RemoveAsync(key, considerUow: true); + + cacheValue = await personCache.GetAsync(key, considerUow: true); + cacheValue.ShouldBeNull(); + + uow.OnCompleted(async () => + { + cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + }); + + await uow.CompleteAsync(); + } + } + + [Fact] + public async Task Cache_Should_Rollback_With_Uow_For_RemoveAsync() + { + const string key = "testkey"; + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + + using (var uow = GetRequiredService().Begin()) + { + await personCache.SetAsync(key, new PersonCacheItem("john"), considerUow: true); + + cacheValue = await personCache.GetAsync(key, considerUow: true); + cacheValue.ShouldNotBeNull(); + cacheValue.Name.ShouldBe("john"); + + await personCache.RemoveAsync(key, considerUow: true); + + cacheValue = await personCache.GetAsync(key, considerUow: true); + cacheValue.ShouldBeNull(); + } + + cacheValue = await personCache.GetAsync(key, considerUow: false); + cacheValue.ShouldBeNull(); + } + + [Fact] + public async Task Cache_Should_Only_Available_In_Uow_For_GetManyAsync() + { + var testkey = "testkey"; + var testkey2 = "testkey2"; + var testKeys = new[] {testkey, testkey2}; + + using (var uow = GetRequiredService().Begin()) + { + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetManyAsync(testKeys, considerUow: true); + cacheValue.Where(x => x.Value != null).ShouldBeEmpty(); + + await personCache.SetManyAsync(new List> + { + new KeyValuePair(testkey, new PersonCacheItem("john")), + new KeyValuePair(testkey2, new PersonCacheItem("jack")) + }, considerUow: true); + + cacheValue = await personCache.GetManyAsync(testKeys, considerUow: true); + cacheValue.Where(x => x.Value != null).ShouldNotBeEmpty(); + cacheValue.ShouldContain(x => x.Value.Name == "john" || x.Value.Name == "jack"); + + cacheValue = await personCache.GetManyAsync(testKeys, considerUow: false); + cacheValue.Where(x => x.Value != null).ShouldBeEmpty(); + + uow.OnCompleted(async () => + { + cacheValue = await personCache.GetManyAsync(testKeys, considerUow: false); + cacheValue.ShouldNotBeEmpty(); + cacheValue.ShouldContain(x => x.Value.Name == "john" || x.Value.Name == "jack"); + }); + + await uow.CompleteAsync(); + } + } + + [Fact] + public async Task Cache_Should_Rollback_With_Uow_For_GetManyAsync() + { + var testkey = "testkey"; + var testkey2 = "testkey2"; + var testKeys = new[] {testkey, testkey2}; + + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetManyAsync(testKeys, considerUow: false); + cacheValue.Where(x => x.Value != null).ShouldBeEmpty(); + + using (var uow = GetRequiredService().Begin()) + { + cacheValue = await personCache.GetManyAsync(testKeys, considerUow: true); + cacheValue.Where(x => x.Value != null).ShouldBeEmpty(); + + await personCache.SetManyAsync(new List> + { + new KeyValuePair(testkey, new PersonCacheItem("john")), + new KeyValuePair(testkey2, new PersonCacheItem("jack")) + }, considerUow: true); + + cacheValue = await personCache.GetManyAsync(testKeys, considerUow: true); + cacheValue.Where(x => x.Value != null).ShouldNotBeEmpty(); + cacheValue.ShouldContain(x => x.Value.Name == "john" || x.Value.Name == "jack"); + + cacheValue = await personCache.GetManyAsync(testKeys, considerUow: false); + cacheValue.Where(x => x.Value != null).ShouldBeEmpty(); + } + + cacheValue = await personCache.GetManyAsync(testKeys, considerUow: false); + cacheValue.Where(x => x.Value != null).ShouldBeEmpty(); + } + + + [Fact] + public async Task Cache_Should_Only_Available_In_Uow_For_SetManyAsync() + { + var testkey = "testkey"; + var testkey2 = "testkey2"; + var testKeys = new[] {testkey, testkey2}; + + using (var uow = GetRequiredService().Begin()) + { + var personCache = GetRequiredService>(); + + await personCache.SetManyAsync(new List> + { + new KeyValuePair(testkey, new PersonCacheItem("john")), + new KeyValuePair(testkey, new PersonCacheItem("jack")) + }, considerUow: true); + + var cacheValue = await personCache.GetManyAsync(testKeys, considerUow: true); + cacheValue.Where(x => x.Value != null).ShouldNotBeEmpty(); + cacheValue.ShouldContain(x => x.Value.Name == "john" || x.Value.Name == "jack"); + + cacheValue = await personCache.GetManyAsync(testKeys, considerUow: false); + cacheValue.Where(x => x.Value != null).ShouldBeEmpty(); + + uow.OnCompleted(async () => + { + var cacheValue = await personCache.GetManyAsync(testKeys, considerUow: false); + cacheValue.Where(x => x.Value != null).ShouldNotBeEmpty(); + cacheValue.ShouldContain(x => x.Value.Name == "john" || x.Value.Name == "jack"); + }); + + await uow.CompleteAsync(); + } + } + + [Fact] + public async Task Cache_Should_Rollback_With_Uow_For_SetManyAsync() + { + var testkey = "testkey"; + var testkey2 = "testkey2"; + var testKeys = new[] {testkey, testkey2}; + + var personCache = GetRequiredService>(); + + var cacheValue = await personCache.GetManyAsync(testKeys, considerUow: false); + cacheValue.Where(x => x.Value != null).ShouldBeEmpty(); + + using (var uow = GetRequiredService().Begin()) + { + await personCache.SetManyAsync(new List> + { + new KeyValuePair(testkey, new PersonCacheItem("john")), + new KeyValuePair(testkey, new PersonCacheItem("jack")) + }, considerUow: true); + + cacheValue = await personCache.GetManyAsync(testKeys, considerUow: true); + cacheValue.Where(x => x.Value != null).ShouldNotBeEmpty(); + cacheValue.ShouldContain(x => x.Value.Name == "john" || x.Value.Name == "jack"); + + cacheValue = await personCache.GetManyAsync(testKeys, considerUow: false); + cacheValue.Where(x => x.Value != null).ShouldBeEmpty(); + } + + cacheValue = await personCache.GetManyAsync(testKeys, considerUow: false); + cacheValue.Where(x => x.Value != null).ShouldBeEmpty(); + } + + [Fact] public async Task Should_Set_And_Get_Multiple_Items_Async() { @@ -317,7 +690,7 @@ namespace Volo.Abp.Caching await personCache.SetManyAsync(new[] { - new KeyValuePair("john", new PersonCacheItem("John Nash")), + new KeyValuePair("john", new PersonCacheItem("John Nash")), new KeyValuePair("thomas", new PersonCacheItem("Thomas Moore")) }); @@ -327,7 +700,7 @@ namespace Volo.Abp.Caching "thomas", "baris" //doesn't exist }); - + cacheItems.Length.ShouldBe(3); cacheItems[0].Key.ShouldBe("john"); cacheItems[0].Value.Name.ShouldBe("John Nash"); @@ -335,9 +708,9 @@ namespace Volo.Abp.Caching cacheItems[1].Value.Name.ShouldBe("Thomas Moore"); cacheItems[2].Key.ShouldBe("baris"); cacheItems[2].Value.ShouldBeNull(); - + (await personCache.GetAsync("john")).Name.ShouldBe("John Nash"); (await personCache.GetAsync("baris")).ShouldBeNull(); } } -} \ No newline at end of file +} diff --git a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/TestMemoryDistributedCache.cs b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/TestMemoryDistributedCache.cs new file mode 100644 index 0000000000..69589d97cd --- /dev/null +++ b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/TestMemoryDistributedCache.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Caching +{ + [DisableConventionalRegistration] + public class TestMemoryDistributedCache : MemoryDistributedCache, ICacheSupportsMultipleItems + { + public TestMemoryDistributedCache(IOptions optionsAccessor) + : base(optionsAccessor) + { + } + + public TestMemoryDistributedCache(IOptions optionsAccessor, ILoggerFactory loggerFactory) + : base(optionsAccessor, loggerFactory) + { + } + + public byte[][] GetMany(IEnumerable keys) + { + var values = new List(); + foreach (var key in keys) + { + values.Add(Get(key)); + } + return values.ToArray(); + } + + public async Task GetManyAsync(IEnumerable keys, CancellationToken token = default) + { + var values = new List(); + foreach (var key in keys) + { + values.Add(await GetAsync(key, token)); + } + return values.ToArray(); + } + + public void SetMany(IEnumerable> items, DistributedCacheEntryOptions options) + { + foreach (var item in items) + { + Set(item.Key, item.Value, options); + } + } + + public async Task SetManyAsync(IEnumerable> items, DistributedCacheEntryOptions options, CancellationToken token = default) + { + foreach (var item in items) + { + await SetAsync(item.Key, item.Value, options, token); + } + } + } +} diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs index 47649ad3c3..d3371f383a 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs @@ -1,4 +1,7 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.MultiTenancy; using Xunit; namespace Volo.Abp.EventBus.Distributed @@ -17,5 +20,29 @@ namespace Volo.Abp.EventBus.Distributed Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); Assert.Equal(3, MySimpleDistributedTransientEventHandler.DisposeCount); } + + [Fact] + public async Task Should_Change_TenantId_If_EventData_Is_MultiTenant() + { + var tenantId = Guid.NewGuid(); + + DistributedEventBus.Subscribe(GetRequiredService()); + + await DistributedEventBus.PublishAsync(new MySimpleEventData(3, tenantId)); + + Assert.Equal(tenantId, MySimpleDistributedSingleInstanceEventHandler.TenantId); + } + + [Fact] + public async Task Should_Change_TenantId_If_Generic_EventData_Is_MultiTenant() + { + var tenantId = Guid.NewGuid(); + + DistributedEventBus.Subscribe>(GetRequiredService()); + + await DistributedEventBus.PublishAsync(new MySimpleEventData(3, tenantId)); + + Assert.Equal(tenantId, MySimpleDistributedSingleInstanceEventHandler.TenantId); + } } } diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/MySimpleDistributedSingleInstanceEventHandler.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/MySimpleDistributedSingleInstanceEventHandler.cs new file mode 100644 index 0000000000..53fe8e347b --- /dev/null +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/MySimpleDistributedSingleInstanceEventHandler.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.EventBus.Distributed +{ + public class MySimpleDistributedSingleInstanceEventHandler : IDistributedEventHandler, IDistributedEventHandler>, ITransientDependency + { + private readonly ICurrentTenant _currentTenant; + + public MySimpleDistributedSingleInstanceEventHandler(ICurrentTenant currentTenant) + { + _currentTenant = currentTenant; + } + + public static Guid? TenantId { get; set; } + + public Task HandleEventAsync(MySimpleEventData eventData) + { + TenantId = _currentTenant.Id; + return Task.CompletedTask; + } + + public Task HandleEventAsync(EntityCreatedEto eventData) + { + TenantId = _currentTenant.Id; + return Task.CompletedTask; + } + } +} diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/EventBus_MultiTenancy_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/EventBus_MultiTenancy_Test.cs new file mode 100644 index 0000000000..62dcb33b42 --- /dev/null +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/EventBus_MultiTenancy_Test.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Entities.Events; +using Volo.Abp.MultiTenancy; +using Xunit; + +namespace Volo.Abp.EventBus.Local +{ + public class EventBus_MultiTenancy_Test : EventBusTestBase + { + [Fact] + public async Task Should_Change_TenantId_If_EventData_Is_MultiTenant() + { + var tenantId = Guid.NewGuid(); + var handler = new MyEventHandler(GetRequiredService()); + + LocalEventBus.Subscribe>(handler); + + await LocalEventBus.PublishAsync(new EntityCreatedEventData(new MyEntity(tenantId))); + + handler.TenantId.ShouldBe(tenantId); + } + + public class MyEntity : Entity, IMultiTenant + { + public override object[] GetKeys() + { + return new object[0]; + } + + public MyEntity() + { + + } + + public MyEntity(Guid? tenantId) + { + TenantId = tenantId; + } + + public Guid? TenantId { get; } + } + + public class MyEventHandler : ILocalEventHandler> + { + private readonly ICurrentTenant _currentTenant; + + public MyEventHandler(ICurrentTenant currentTenant) + { + _currentTenant = currentTenant; + } + + public Guid? TenantId { get; set; } + + public Task HandleEventAsync(EntityChangedEventData eventData) + { + TenantId = _currentTenant.Id; + return Task.CompletedTask; + } + } + } +} diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/MySimpleEventData.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/MySimpleEventData.cs index 08142b3355..45a3e7f237 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/MySimpleEventData.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/MySimpleEventData.cs @@ -1,12 +1,18 @@ +using System; +using Volo.Abp.MultiTenancy; + namespace Volo.Abp.EventBus { - public class MySimpleEventData + public class MySimpleEventData : IMultiTenant { public int Value { get; set; } - public MySimpleEventData(int value) + public Guid? TenantId { get; } + + public MySimpleEventData(int value, Guid? tenantId = null) { Value = value; + TenantId = tenantId; } } -} \ No newline at end of file +} diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs index 5afe312a6e..297e54cd69 100644 --- a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs +++ b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs @@ -132,7 +132,7 @@ namespace Volo.Abp.Account.Web.Pages.Account true ); - await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() { Identity = IdentitySecurityLogIdentityConsts.Identity, Action = result.ToIdentitySecurityLogAction(), diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLogoutModel.cs b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLogoutModel.cs index b25b34799d..bec2ede369 100644 --- a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLogoutModel.cs +++ b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLogoutModel.cs @@ -20,7 +20,7 @@ namespace Volo.Abp.Account.Web.Pages.Account public override async Task OnGetAsync() { - await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() { Identity = IdentitySecurityLogIdentityConsts.Identity, Action = IdentitySecurityLogActionConsts.Logout diff --git a/modules/account/src/Volo.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs b/modules/account/src/Volo.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs index aa22882701..575853f410 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs +++ b/modules/account/src/Volo.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs @@ -6,10 +6,8 @@ using Volo.Abp.Account.Localization; using Volo.Abp.Account.Settings; using Volo.Abp.Account.Web.Areas.Account.Controllers.Models; using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.EventBus.Local; using Volo.Abp.Identity; using Volo.Abp.Identity.AspNetCore; -using Volo.Abp.SecurityLog; using Volo.Abp.Settings; using Volo.Abp.Validation; using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; @@ -28,22 +26,20 @@ namespace Volo.Abp.Account.Web.Areas.Account.Controllers protected SignInManager SignInManager { get; } protected IdentityUserManager UserManager { get; } protected ISettingProvider SettingProvider { get; } - - protected ILocalEventBus LocalEventBus { get; } + protected IdentitySecurityLogManager IdentitySecurityLogManager { get; } public AccountController( SignInManager signInManager, IdentityUserManager userManager, ISettingProvider settingProvider, - ISecurityLogManager securityLogManager, - ILocalEventBus localEventBus) + IdentitySecurityLogManager identitySecurityLogManager) { LocalizationResource = typeof(AccountResource); SignInManager = signInManager; UserManager = userManager; SettingProvider = settingProvider; - LocalEventBus = localEventBus; + IdentitySecurityLogManager = identitySecurityLogManager; } [HttpPost] @@ -62,7 +58,7 @@ namespace Volo.Abp.Account.Web.Areas.Account.Controllers true ); - await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() { Identity = IdentitySecurityLogIdentityConsts.Identity, Action = signInResult.ToIdentitySecurityLogAction(), @@ -76,13 +72,13 @@ namespace Volo.Abp.Account.Web.Areas.Account.Controllers [Route("logout")] public virtual async Task Logout() { - await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() { Identity = IdentitySecurityLogIdentityConsts.Identity, Action = IdentitySecurityLogActionConsts.Logout }); - await SignInManager.SignOutAsync(); + await SignInManager.SignOutAsync(); } [HttpPost] diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/AccountPageModel.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/AccountPageModel.cs index e645c9c9cf..7e79442bd7 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/AccountPageModel.cs +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/AccountPageModel.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Volo.Abp.Account.Localization; using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; -using Volo.Abp.EventBus.Local; using Volo.Abp.Identity; using IdentityUser = Volo.Abp.Identity.IdentityUser; @@ -15,7 +14,7 @@ namespace Volo.Abp.Account.Web.Pages.Account { public SignInManager SignInManager { get; set; } public IdentityUserManager UserManager { get; set; } - public ILocalEventBus LocalEventBus { get; set; } + public IdentitySecurityLogManager IdentitySecurityLogManager { get; set; } protected AccountPageModel() { diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs index 05946182db..9d6c540ce5 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs @@ -98,7 +98,7 @@ namespace Volo.Abp.Account.Web.Pages.Account true ); - await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() { Identity = IdentitySecurityLogIdentityConsts.Identity, Action = result.ToIdentitySecurityLogAction(), @@ -194,7 +194,7 @@ namespace Volo.Abp.Account.Web.Pages.Account if (!result.Succeeded) { - await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() { Identity = IdentitySecurityLogIdentityConsts.IdentityExternal, Action = "Login" + result @@ -224,7 +224,7 @@ namespace Volo.Abp.Account.Web.Pages.Account await SignInManager.SignInAsync(user, false); - await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() { Identity = IdentitySecurityLogIdentityConsts.IdentityExternal, Action = result.ToIdentitySecurityLogAction(), diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Logout.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Logout.cshtml.cs index fe7361e4ce..5923827a24 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Logout.cshtml.cs +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Logout.cshtml.cs @@ -16,7 +16,7 @@ namespace Volo.Abp.Account.Web.Pages.Account public virtual async Task OnGetAsync() { - await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() { Identity = IdentitySecurityLogIdentityConsts.Identity, Action = IdentitySecurityLogActionConsts.Logout diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs index 26388fdaf9..5b892a91bb 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs @@ -5,6 +5,7 @@ using Volo.Abp.Caching; using Volo.Abp.DependencyInjection; using Volo.Abp.Features; using Volo.Abp.Guids; +using Volo.Abp.Uow; namespace Volo.Abp.FeatureManagement { @@ -27,12 +28,14 @@ namespace Volo.Abp.FeatureManagement FeatureDefinitionManager = featureDefinitionManager; } + [UnitOfWork] public virtual async Task GetOrNullAsync(string name, string providerName, string providerKey) { var cacheItem = await GetCacheItemAsync(name, providerName, providerKey); return cacheItem.Value; } + [UnitOfWork] public virtual async Task SetAsync(string name, string value, string providerName, string providerKey) { var featureValue = await FeatureValueRepository.FindAsync(name, providerName, providerKey); @@ -46,21 +49,25 @@ namespace Volo.Abp.FeatureManagement featureValue.Value = value; await FeatureValueRepository.UpdateAsync(featureValue); } + + await Cache.SetAsync(CalculateCacheKey(name, providerName, providerKey), new FeatureValueCacheItem(featureValue?.Value), considerUow: true); } + [UnitOfWork] public virtual async Task DeleteAsync(string name, string providerName, string providerKey) { var featureValues = await FeatureValueRepository.FindAllAsync(name, providerName, providerKey); foreach (var featureValue in featureValues) { await FeatureValueRepository.DeleteAsync(featureValue); + await Cache.RemoveAsync(CalculateCacheKey(name, providerName, providerKey), considerUow: true); } } protected virtual async Task GetCacheItemAsync(string name, string providerName, string providerKey) { var cacheKey = CalculateCacheKey(name, providerName, providerKey); - var cacheItem = await Cache.GetAsync(cacheKey); + var cacheItem = await Cache.GetAsync(cacheKey, considerUow: true); if (cacheItem != null) { @@ -68,28 +75,28 @@ namespace Volo.Abp.FeatureManagement } cacheItem = new FeatureValueCacheItem(null); - + await SetCacheItemsAsync(providerName, providerKey, name, cacheItem); return cacheItem; } - + private async Task SetCacheItemsAsync( - string providerName, - string providerKey, - string currentName, + string providerName, + string providerKey, + string currentName, FeatureValueCacheItem currentCacheItem) { var featureDefinitions = FeatureDefinitionManager.GetAll(); var featuresDictionary = (await FeatureValueRepository.GetListAsync(providerName, providerKey)) .ToDictionary(s => s.Name, s => s.Value); - - var cacheItems = new List>(); - + + var cacheItems = new List>(); + foreach (var featureDefinition in featureDefinitions) { var featureValue = featuresDictionary.GetOrDefault(featureDefinition.Name); - + cacheItems.Add( new KeyValuePair( CalculateCacheKey(featureDefinition.Name, providerName, providerKey), @@ -103,7 +110,7 @@ namespace Volo.Abp.FeatureManagement } } - await Cache.SetManyAsync(cacheItems); + await Cache.SetManyAsync(cacheItems, considerUow: true); } protected virtual string CalculateCacheKey(string name, string providerName, string providerKey) diff --git a/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/FeatureManagementStore_Tests.cs b/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/FeatureManagementStore_Tests.cs index a6f040dde3..a5250f1804 100644 --- a/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/FeatureManagementStore_Tests.cs +++ b/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/FeatureManagementStore_Tests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Shouldly; using Volo.Abp.Features; using Volo.Abp.Modularity; +using Volo.Abp.Uow; using Xunit; namespace Volo.Abp.FeatureManagement @@ -14,11 +15,13 @@ namespace Volo.Abp.FeatureManagement { private IFeatureManagementStore FeatureManagementStore { get; set; } private IFeatureValueRepository FeatureValueRepository { get; set; } + private IUnitOfWorkManager UnitOfWorkManager { get; set; } protected FeatureManagementStore_Tests() { FeatureManagementStore = GetRequiredService(); FeatureValueRepository = GetRequiredService(); + UnitOfWorkManager = GetRequiredService(); } [Fact] @@ -73,6 +76,30 @@ namespace Volo.Abp.FeatureManagement TestEditionIds.Regular.ToString())).Value.ShouldBe(false.ToString().ToUpperInvariant()); } + [Fact] + public async Task Set_In_UnitOfWork_Should_Be_Consistent() + { + using (UnitOfWorkManager.Begin()) + { + // Arrange + (await FeatureManagementStore.GetOrNullAsync(TestFeatureDefinitionProvider.SocialLogins, + EditionFeatureValueProvider.ProviderName, + TestEditionIds.Regular.ToString())).ShouldNotBeNull(); + + + // Act + await FeatureManagementStore.SetAsync(TestFeatureDefinitionProvider.SocialLogins, + false.ToString().ToUpperInvariant(), + EditionFeatureValueProvider.ProviderName, + TestEditionIds.Regular.ToString()); + + // Assert + (await FeatureManagementStore.GetOrNullAsync(TestFeatureDefinitionProvider.SocialLogins, + EditionFeatureValueProvider.ProviderName, + TestEditionIds.Regular.ToString())).ShouldBe(false.ToString().ToUpperInvariant()); + } + } + [Fact] public async Task DeleteAsync() { @@ -94,5 +121,29 @@ namespace Volo.Abp.FeatureManagement } + + [Fact] + public async Task Delete_In_UnitOfWork_Should_Be_Consistent() + { + using (var uow = UnitOfWorkManager.Begin()) + { + // Arrange + (await FeatureManagementStore.GetOrNullAsync(TestFeatureDefinitionProvider.SocialLogins, + EditionFeatureValueProvider.ProviderName, + TestEditionIds.Regular.ToString())).ShouldNotBeNull(); + + // Act + await FeatureManagementStore.DeleteAsync(TestFeatureDefinitionProvider.SocialLogins, + EditionFeatureValueProvider.ProviderName, + TestEditionIds.Regular.ToString()); + + await uow.SaveChangesAsync(); + + // Assert + (await FeatureManagementStore.GetOrNullAsync(TestFeatureDefinitionProvider.SocialLogins, + EditionFeatureValueProvider.ProviderName, + TestEditionIds.Regular.ToString())).ShouldBeNull(); + } + } } } diff --git a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/AbpIdentityApplicationContractsModule.cs b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/AbpIdentityApplicationContractsModule.cs index 107acba9d1..43481ff30a 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/AbpIdentityApplicationContractsModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/AbpIdentityApplicationContractsModule.cs @@ -1,6 +1,8 @@ using Volo.Abp.Application; using Volo.Abp.Authorization; using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; +using Volo.Abp.ObjectExtending.Modularity; using Volo.Abp.PermissionManagement; using Volo.Abp.Users; @@ -19,5 +21,26 @@ namespace Volo.Abp.Identity { } + + public override void PostConfigureServices(ServiceConfigurationContext context) + { + ModuleExtensionConfigurationHelper + .ApplyEntityConfigurationToApi( + IdentityModuleExtensionConsts.ModuleName, + IdentityModuleExtensionConsts.EntityNames.Role, + getApiTypes: new[] { typeof(IdentityRoleDto) }, + createApiTypes: new[] { typeof(IdentityRoleCreateDto) }, + updateApiTypes: new[] { typeof(IdentityRoleUpdateDto) } + ); + + ModuleExtensionConfigurationHelper + .ApplyEntityConfigurationToApi( + IdentityModuleExtensionConsts.ModuleName, + IdentityModuleExtensionConsts.EntityNames.User, + getApiTypes: new[] { typeof(IdentityUserDto) }, + createApiTypes: new[] { typeof(IdentityUserCreateDto) }, + updateApiTypes: new[] { typeof(IdentityUserUpdateDto) } + ); + } } } \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityRoleCreateOrUpdateDtoBase.cs b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityRoleCreateOrUpdateDtoBase.cs index 491e82d766..6b68c43568 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityRoleCreateOrUpdateDtoBase.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityRoleCreateOrUpdateDtoBase.cs @@ -13,5 +13,10 @@ namespace Volo.Abp.Identity public bool IsDefault { get; set; } public bool IsPublic { get; set; } + + protected IdentityRoleCreateOrUpdateDtoBase() : base(false) + { + + } } } \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityUserCreateOrUpdateDtoBase.cs b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityUserCreateOrUpdateDtoBase.cs index e06ea8b987..41981b2ca8 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityUserCreateOrUpdateDtoBase.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityUserCreateOrUpdateDtoBase.cs @@ -32,5 +32,10 @@ namespace Volo.Abp.Identity [CanBeNull] public string[] RoleNames { get; set; } + + protected IdentityUserCreateOrUpdateDtoBase() : base(false) + { + + } } } \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogEvent.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogContext.cs similarity index 66% rename from modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogEvent.cs rename to modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogContext.cs index faac2d5033..740688ff84 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogEvent.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogContext.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; -using Volo.Abp.MultiTenancy; namespace Volo.Abp.Identity { - public class IdentitySecurityLogEvent : IMultiTenant + public class IdentitySecurityLogContext { - public Guid? TenantId { get; set; } - public string Identity { get; set; } public string Action { get; set; } @@ -18,12 +15,12 @@ namespace Volo.Abp.Identity public Dictionary ExtraProperties { get; } - public IdentitySecurityLogEvent() + public IdentitySecurityLogContext() { ExtraProperties = new Dictionary(); } - public virtual IdentitySecurityLogEvent WithProperty(string key, object value) + public virtual IdentitySecurityLogContext WithProperty(string key, object value) { ExtraProperties[key] = value; return this; diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogHandler.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogManager.cs similarity index 74% rename from modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogHandler.cs rename to modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogManager.cs index 5000d4d288..bdad21df13 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogHandler.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentitySecurityLogManager.cs @@ -2,15 +2,13 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Volo.Abp.DependencyInjection; -using Volo.Abp.EventBus; using Volo.Abp.Security.Claims; using Volo.Abp.SecurityLog; -using Volo.Abp.Uow; using Volo.Abp.Users; namespace Volo.Abp.Identity { - public class IdentitySecurityLogHandler : ILocalEventHandler, ITransientDependency + public class IdentitySecurityLogManager : ITransientDependency { protected ISecurityLogManager SecurityLogManager { get; } protected IdentityUserManager UserManager { get; } @@ -18,7 +16,7 @@ namespace Volo.Abp.Identity protected IUserClaimsPrincipalFactory UserClaimsPrincipalFactory { get; } protected ICurrentUser CurrentUser { get; } - public IdentitySecurityLogHandler( + public IdentitySecurityLogManager( ISecurityLogManager securityLogManager, IdentityUserManager userManager, ICurrentPrincipalAccessor currentPrincipalAccessor, @@ -32,24 +30,24 @@ namespace Volo.Abp.Identity CurrentUser = currentUser; } - public async Task HandleEventAsync(IdentitySecurityLogEvent eventData) + public async Task SaveAsync(IdentitySecurityLogContext context) { Action securityLogAction = securityLog => { - securityLog.Identity = eventData.Identity; - securityLog.Action = eventData.Action; + securityLog.Identity = context.Identity; + securityLog.Action = context.Action; - if (securityLog.UserName.IsNullOrWhiteSpace()) + if (!context.UserName.IsNullOrWhiteSpace()) { - securityLog.UserName = eventData.UserName; + securityLog.UserName = context.UserName; } - if (securityLog.ClientId.IsNullOrWhiteSpace()) + if (!context.ClientId.IsNullOrWhiteSpace()) { - securityLog.ClientId = eventData.ClientId; + securityLog.ClientId = context.ClientId; } - foreach (var property in eventData.ExtraProperties) + foreach (var property in context.ExtraProperties) { securityLog.ExtraProperties[property.Key] = property.Value; } @@ -61,13 +59,13 @@ namespace Volo.Abp.Identity } else { - if (eventData.UserName.IsNullOrWhiteSpace()) + if (context.UserName.IsNullOrWhiteSpace()) { await SecurityLogManager.SaveAsync(securityLogAction); } else { - var user = await UserManager.FindByNameAsync(eventData.UserName); + var user = await UserManager.FindByNameAsync(context.UserName); if (user != null) { using (CurrentPrincipalAccessor.Change(await UserClaimsPrincipalFactory.CreateAsync(user))) diff --git a/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebAutoMapperProfile.cs b/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebAutoMapperProfile.cs index b0154f543f..f7dfc69099 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebAutoMapperProfile.cs +++ b/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebAutoMapperProfile.cs @@ -22,7 +22,7 @@ namespace Volo.Abp.Identity.Web //CreateModal CreateMap() - .Ignore(x => x.ExtraProperties) + .MapExtraProperties() .ForMember(dest => dest.RoleNames, opt => opt.Ignore()); CreateMap() @@ -30,7 +30,7 @@ namespace Volo.Abp.Identity.Web //EditModal CreateMap() - .Ignore(x => x.ExtraProperties) + .MapExtraProperties() .ForMember(dest => dest.RoleNames, opt => opt.Ignore()); CreateMap() @@ -44,11 +44,11 @@ namespace Volo.Abp.Identity.Web //CreateModal CreateMap() - .Ignore(x => x.ExtraProperties); + .MapExtraProperties(); //EditModal CreateMap() - .Ignore(x => x.ExtraProperties); + .MapExtraProperties(); } } } diff --git a/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebModule.cs b/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebModule.cs index 79b636418c..23a4529502 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebModule.cs @@ -7,6 +7,8 @@ using Volo.Abp.AutoMapper; using Volo.Abp.Identity.Localization; using Volo.Abp.Identity.Web.Navigation; using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; +using Volo.Abp.ObjectExtending.Modularity; using Volo.Abp.PermissionManagement.Web; using Volo.Abp.UI.Navigation; using Volo.Abp.VirtualFileSystem; @@ -62,5 +64,24 @@ namespace Volo.Abp.Identity.Web options.Conventions.AuthorizePage("/Identity/Roles/EditModal", IdentityPermissions.Roles.Update); }); } + + public override void PostConfigureServices(ServiceConfigurationContext context) + { + ModuleExtensionConfigurationHelper + .ApplyEntityConfigurationToUi( + IdentityModuleExtensionConsts.ModuleName, + IdentityModuleExtensionConsts.EntityNames.Role, + createFormTypes: new[] { typeof(Volo.Abp.Identity.Web.Pages.Identity.Roles.CreateModalModel.RoleInfoModel) }, + editFormTypes: new[] { typeof(Volo.Abp.Identity.Web.Pages.Identity.Roles.EditModalModel.RoleInfoModel) } + ); + + ModuleExtensionConfigurationHelper + .ApplyEntityConfigurationToUi( + IdentityModuleExtensionConsts.ModuleName, + IdentityModuleExtensionConsts.EntityNames.User, + createFormTypes: new[] { typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.CreateModalModel.UserInfoViewModel) }, + editFormTypes: new[] { typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.EditModalModel.UserInfoViewModel) } + ); + } } } diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/CreateModal.cshtml b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/CreateModal.cshtml index ce075d0b58..72ea9da317 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/CreateModal.cshtml +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/CreateModal.cshtml @@ -1,9 +1,14 @@ @page @using Microsoft.AspNetCore.Mvc.Localization +@using Microsoft.Extensions.Localization @using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal @using Volo.Abp.Identity.Localization -@model Volo.Abp.Identity.Web.Pages.Identity.Roles.CreateModalModel +@using Volo.Abp.Identity.Web.Pages.Identity.Roles +@using Volo.Abp.Localization +@using Volo.Abp.ObjectExtending +@model CreateModalModel @inject IHtmlLocalizer L +@inject IStringLocalizerFactory StringLocalizerFactory @{ Layout = null; } @@ -12,8 +17,27 @@ + + + + @foreach (var propertyInfo in ObjectExtensionManager.Instance.GetProperties()) + { + if (propertyInfo.Type.IsEnum) + { + + } + else + { + + } + } diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/CreateModal.cshtml.cs b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/CreateModal.cshtml.cs index df86acd94d..79787da08b 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/CreateModal.cshtml.cs +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/CreateModal.cshtml.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Volo.Abp.ObjectExtending; using Volo.Abp.Validation; namespace Volo.Abp.Identity.Web.Pages.Identity.Roles @@ -19,6 +20,8 @@ namespace Volo.Abp.Identity.Web.Pages.Identity.Roles public virtual Task OnGetAsync() { + Role = new RoleInfoModel(); + return Task.FromResult(Page()); } @@ -32,7 +35,7 @@ namespace Volo.Abp.Identity.Web.Pages.Identity.Roles return NoContent(); } - public class RoleInfoModel + public class RoleInfoModel : ExtensibleObject { [Required] [DynamicStringLength(typeof(IdentityRoleConsts), nameof(IdentityRoleConsts.MaxNameLength))] diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/EditModal.cshtml b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/EditModal.cshtml index 0a3cd3668d..8ef33b32cc 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/EditModal.cshtml +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/EditModal.cshtml @@ -1,9 +1,14 @@ @page @using Microsoft.AspNetCore.Mvc.Localization +@using Microsoft.Extensions.Localization @using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal @using Volo.Abp.Identity.Localization -@model Volo.Abp.Identity.Web.Pages.Identity.Roles.EditModalModel +@using Volo.Abp.Identity.Web.Pages.Identity.Roles +@using Volo.Abp.Localization +@using Volo.Abp.ObjectExtending +@model EditModalModel @inject IHtmlLocalizer L +@inject IStringLocalizerFactory StringLocalizerFactory @{ Layout = null; } @@ -12,7 +17,9 @@ + + @if (Model.Role.IsStatic) { @@ -21,8 +28,27 @@ { } + + + + @foreach (var propertyInfo in ObjectExtensionManager.Instance.GetProperties()) + { + if (propertyInfo.Type.IsEnum) + { + + } + else + { + + } + } diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/EditModal.cshtml.cs b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/EditModal.cshtml.cs index 8a004e95a5..14236c1466 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/EditModal.cshtml.cs +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/EditModal.cshtml.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Volo.Abp.Domain.Entities; +using Volo.Abp.ObjectExtending; using Volo.Abp.Validation; namespace Volo.Abp.Identity.Web.Pages.Identity.Roles @@ -36,7 +37,7 @@ namespace Volo.Abp.Identity.Web.Pages.Identity.Roles return NoContent(); } - public class RoleInfoModel : IHasConcurrencyStamp + public class RoleInfoModel : ExtensibleObject, IHasConcurrencyStamp { [HiddenInput] public Guid Id { get; set; } diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/Index.cshtml b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/Index.cshtml index 4658f44013..734fc40fe5 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/Index.cshtml +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/Index.cshtml @@ -41,13 +41,6 @@ - - - - @L["Actions"] - @L["RoleName"] - - - + \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/index.js b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/index.js index 4331ab4229..d66d7240e3 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/index.js +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/index.js @@ -12,76 +12,76 @@ abp.appPath + 'Identity/Roles/CreateModal' ); - $(function () { - var _$wrapper = $('#IdentityRolesWrapper'); - var _$table = _$wrapper.find('table'); + var _dataTable = null; - var _dataTable = _$table.DataTable( - abp.libs.datatables.normalizeConfiguration({ - order: [[1, 'asc']], - searching: false, - processing: true, - serverSide: true, - scrollX: true, - paging: true, - ajax: abp.libs.datatables.createAjax( - _identityRoleAppService.getList - ), - columnDefs: [ + abp.ui.extensions.entityActions.get('identity.role').addContributor( + function(actionList) { + return actionList.addManyTail( + [ { - rowAction: { - items: [ - { - text: l('Edit'), - visible: abp.auth.isGranted( - 'AbpIdentity.Roles.Update' - ), - action: function (data) { - _editModal.open({ - id: data.record.id, - }); - }, - }, - { - text: l('Permissions'), - visible: abp.auth.isGranted( - 'AbpIdentity.Roles.ManagePermissions' - ), - action: function (data) { - _permissionsModal.open({ - providerName: 'R', - providerKey: data.record.name, - }); - }, - }, - { - text: l('Delete'), - visible: function (data) { - return ( - !data.isStatic && - abp.auth.isGranted( - 'AbpIdentity.Roles.Delete' - ) - ); //TODO: Check permission - }, - confirmMessage: function (data) { - return l( - 'RoleDeletionConfirmationMessage', - data.record.name - ); - }, - action: function (data) { - _identityRoleAppService - .delete(data.record.id) - .then(function () { - _dataTable.ajax.reload(); - }); - }, - }, - ], + text: l('Edit'), + visible: abp.auth.isGranted( + 'AbpIdentity.Roles.Update' + ), + action: function (data) { + _editModal.open({ + id: data.record.id, + }); + }, + }, + { + text: l('Permissions'), + visible: abp.auth.isGranted( + 'AbpIdentity.Roles.ManagePermissions' + ), + action: function (data) { + _permissionsModal.open({ + providerName: 'R', + providerKey: data.record.name, + }); + }, + }, + { + text: l('Delete'), + visible: function (data) { + return ( + !data.isStatic && + abp.auth.isGranted( + 'AbpIdentity.Roles.Delete' + ) + ); //TODO: Check permission + }, + confirmMessage: function (data) { + return l( + 'RoleDeletionConfirmationMessage', + data.record.name + ); }, + action: function (data) { + _identityRoleAppService + .delete(data.record.id) + .then(function () { + _dataTable.ajax.reload(); + }); + }, + } + ] + ); + } + ); + + abp.ui.extensions.tableColumns.get('identity.role').addContributor( + function (columnList) { + columnList.addManyTail( + [ + { + title: l("Actions"), + rowAction: { + items: abp.ui.extensions.entityActions.get('identity.role').actions.toArray() + } }, { + title: l('RoleName'), data: 'name', render: function (data, type, row) { var name = '' + data + ''; @@ -99,8 +99,29 @@ } return name; }, - }, - ], + } + ] + ); + }, + 0 //adds as the first contributor + ); + + $(function () { + var _$wrapper = $('#IdentityRolesWrapper'); + var _$table = _$wrapper.find('table'); + + _dataTable = _$table.DataTable( + abp.libs.datatables.normalizeConfiguration({ + order: [[1, 'asc']], + searching: false, + processing: true, + serverSide: true, + scrollX: true, + paging: true, + ajax: abp.libs.datatables.createAjax( + _identityRoleAppService.getList + ), + columnDefs: abp.ui.extensions.tableColumns.get('identity.role').columns.toArray() }) ); diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/CreateModal.cshtml b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/CreateModal.cshtml index d20d0a0026..b171b785a2 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/CreateModal.cshtml +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/CreateModal.cshtml @@ -1,9 +1,14 @@ @page @using Microsoft.AspNetCore.Mvc.Localization +@using Microsoft.Extensions.Localization @using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal @using Volo.Abp.Identity.Localization -@model Volo.Abp.Identity.Web.Pages.Identity.Users.CreateModalModel +@using Volo.Abp.Identity.Web.Pages.Identity.Users +@using Volo.Abp.Localization +@using Volo.Abp.ObjectExtending +@model CreateModalModel @inject IHtmlLocalizer L +@inject IStringLocalizerFactory StringLocalizerFactory @{ Layout = null; } @@ -22,6 +27,23 @@ + + @foreach (var propertyInfo in ObjectExtensionManager.Instance.GetProperties()) + { + if (propertyInfo.Type.IsEnum) + { + + } + else + { + + } + } @for (var i = 0; i < Model.Roles.Length; i++) diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/CreateModal.cshtml.cs b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/CreateModal.cshtml.cs index 6149c676b8..a868890632 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/CreateModal.cshtml.cs +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/CreateModal.cshtml.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Volo.Abp.Auditing; using Volo.Abp.Application.Dtos; +using Volo.Abp.ObjectExtending; using Volo.Abp.Validation; namespace Volo.Abp.Identity.Web.Pages.Identity.Users @@ -52,7 +53,7 @@ namespace Volo.Abp.Identity.Web.Pages.Identity.Users return NoContent(); } - public class UserInfoViewModel + public class UserInfoViewModel : ExtensibleObject { [Required] [DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxUserNameLength))] diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml index 26962350e0..56ac3929f1 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml @@ -1,9 +1,14 @@ @page @using Microsoft.AspNetCore.Mvc.Localization +@using Microsoft.Extensions.Localization @using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal @using Volo.Abp.Identity.Localization -@model Volo.Abp.Identity.Web.Pages.Identity.Users.EditModalModel +@using Volo.Abp.Identity.Web.Pages.Identity.Users +@using Volo.Abp.Localization +@using Volo.Abp.ObjectExtending +@model EditModalModel @inject IHtmlLocalizer L +@inject IStringLocalizerFactory StringLocalizerFactory @{ Layout = null; } @@ -24,6 +29,24 @@ + + @foreach (var propertyInfo in ObjectExtensionManager.Instance.GetProperties()) + { + if (propertyInfo.Type.IsEnum) + { + + } + else + { + + } + } + @for (var i = 0; i < Model.Roles.Length; i++) diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs index 08c5ae0dd9..a2336c2b8e 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc; using Volo.Abp.Auditing; using Volo.Abp.Application.Dtos; using Volo.Abp.Domain.Entities; +using Volo.Abp.ObjectExtending; using Volo.Abp.Validation; namespace Volo.Abp.Identity.Web.Pages.Identity.Users @@ -55,7 +56,7 @@ namespace Volo.Abp.Identity.Web.Pages.Identity.Users return NoContent(); } - public class UserInfoViewModel : IHasConcurrencyStamp + public class UserInfoViewModel : ExtensibleObject, IHasConcurrencyStamp { [HiddenInput] public Guid Id { get; set; } diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/Index.cshtml b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/Index.cshtml index 23f7747ac8..ee9192d58f 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/Index.cshtml +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/Index.cshtml @@ -42,15 +42,6 @@ - - - - @L["Actions"] - @L["UserName"] - @L["EmailAddress"] - @L["PhoneNumber"] - - - + \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/index.js b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/index.js index 53f66ddb5e..71675d8b8e 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/index.js +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/index.js @@ -12,10 +12,91 @@ abp.appPath + 'AbpPermissionManagement/PermissionManagementModal' ); + var _dataTable = null; + + abp.ui.extensions.entityActions.get('identity.user').addContributor( + function(actionList) { + return actionList.addManyTail( + [ + { + text: l('Edit'), + visible: abp.auth.isGranted( + 'AbpIdentity.Users.Update' + ), + action: function (data) { + _editModal.open({ + id: data.record.id, + }); + }, + }, + { + text: l('Permissions'), + visible: abp.auth.isGranted( + 'AbpIdentity.Users.ManagePermissions' + ), + action: function (data) { + _permissionsModal.open({ + providerName: 'U', + providerKey: data.record.id, + }); + }, + }, + { + text: l('Delete'), + visible: abp.auth.isGranted( + 'AbpIdentity.Users.Delete' + ), + confirmMessage: function (data) { + return l( + 'UserDeletionConfirmationMessage', + data.record.userName + ); + }, + action: function (data) { + _identityUserAppService + .delete(data.record.id) + .then(function () { + _dataTable.ajax.reload(); + }); + }, + } + ] + ); + } + ); + + abp.ui.extensions.tableColumns.get('identity.user').addContributor( + function (columnList) { + columnList.addManyTail( + [ + { + title: l("Actions"), + rowAction: { + items: abp.ui.extensions.entityActions.get('identity.user').actions.toArray() + } + }, + { + title: l('UserName'), + data: 'userName', + }, + { + title: l('EmailAddress'), + data: 'email', + }, + { + title: l('PhoneNumber'), + data: 'phoneNumber', + } + ] + ); + }, + 0 //adds as the first contributor + ); + $(function () { var _$wrapper = $('#IdentityUsersWrapper'); var _$table = _$wrapper.find('table'); - var _dataTable = _$table.DataTable( + _dataTable = _$table.DataTable( abp.libs.datatables.normalizeConfiguration({ order: [[1, 'asc']], processing: true, @@ -25,65 +106,7 @@ ajax: abp.libs.datatables.createAjax( _identityUserAppService.getList ), - columnDefs: [ - { - rowAction: { - items: [ - { - text: l('Edit'), - visible: abp.auth.isGranted( - 'AbpIdentity.Users.Update' - ), - action: function (data) { - _editModal.open({ - id: data.record.id, - }); - }, - }, - { - text: l('Permissions'), - visible: abp.auth.isGranted( - 'AbpIdentity.Users.ManagePermissions' - ), - action: function (data) { - _permissionsModal.open({ - providerName: 'U', - providerKey: data.record.id, - }); - }, - }, - { - text: l('Delete'), - visible: abp.auth.isGranted( - 'AbpIdentity.Users.Delete' - ), - confirmMessage: function (data) { - return l( - 'UserDeletionConfirmationMessage', - data.record.userName - ); - }, - action: function (data) { - _identityUserAppService - .delete(data.record.id) - .then(function () { - _dataTable.ajax.reload(); - }); - }, - }, - ], - }, - }, - { - data: 'userName', - }, - { - data: 'email', - }, - { - data: 'phoneNumber', - }, - ], + columnDefs: abp.ui.extensions.tableColumns.get('identity.user').columns.toArray() }) ); diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/IdentityServerSecurityLogActionConsts.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/IdentityServerSecurityLogActionConsts.cs new file mode 100644 index 0000000000..47ad6b21d5 --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/IdentityServerSecurityLogActionConsts.cs @@ -0,0 +1,19 @@ +namespace Volo.Abp.IdentityServer +{ + public class IdentityServerSecurityLogActionConsts + { + public static string LoginSucceeded { get; set; } = "LoginSucceeded"; + + public static string LoginLockedout { get; set; } = "LoginLockedout"; + + public static string LoginNotAllowed { get; set; } = "LoginNotAllowed"; + + public static string LoginRequiresTwoFactor { get; set; } = "LoginRequiresTwoFactor"; + + public static string LoginFailed { get; set; } = "LoginFailed"; + + public static string LoginInvalidUserName { get; set; } = "LoginInvalidUserName"; + + public static string LoginInvalidUserNameOrPassword { get; set; } = "LoginInvalidUserNameOrPassword"; + } +} diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/IdentityServerSecurityLogIdentityConsts.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/IdentityServerSecurityLogIdentityConsts.cs new file mode 100644 index 0000000000..4a5f96e0e2 --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/IdentityServerSecurityLogIdentityConsts.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.IdentityServer +{ + public class IdentityServerSecurityLogIdentityConsts + { + public static string IdentityServer { get; set; } = "IdentityServer"; + } +} diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs index e45df9b3f7..90910e1894 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs @@ -10,6 +10,7 @@ using IdentityServer4.Validation; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; +using Volo.Abp.Identity; using Volo.Abp.IdentityServer.Localization; using Volo.Abp.Security.Claims; using Volo.Abp.Uow; @@ -23,18 +24,21 @@ namespace Volo.Abp.IdentityServer.AspNetIdentity protected SignInManager SignInManager { get; } protected IEventService Events { get; } protected UserManager UserManager { get; } + protected IdentitySecurityLogManager IdentitySecurityLogManager { get; } protected ILogger> Logger { get; } protected IStringLocalizer Localizer { get; } public AbpResourceOwnerPasswordValidator( UserManager userManager, SignInManager signInManager, + IdentitySecurityLogManager identitySecurityLogManager, IEventService events, - ILogger> logger, + ILogger> logger, IStringLocalizer localizer) { UserManager = userManager; SignInManager = signInManager; + IdentitySecurityLogManager = identitySecurityLogManager; Events = events; Logger = logger; Localizer = localizer; @@ -71,6 +75,12 @@ namespace Volo.Abp.IdentityServer.AspNetIdentity additionalClaims.ToArray() ); + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() + { + Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, + Action = result.ToIdentitySecurityLogAction(), + }); + return; } else if (result.IsLockedOut) @@ -91,12 +101,25 @@ namespace Volo.Abp.IdentityServer.AspNetIdentity await Events.RaiseAsync(new UserLoginFailureEvent(context.UserName, "invalid credentials", interactive: false)); errorDescription = Localizer["InvalidUserNameOrPassword"]; } + + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() + { + Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, + Action = result.ToIdentitySecurityLogAction(), + UserName = context.UserName + }); } else { Logger.LogInformation("No user found matching username: {username}", context.UserName); await Events.RaiseAsync(new UserLoginFailureEvent(context.UserName, "invalid username", interactive: false)); errorDescription = Localizer["InvalidUsername"]; + + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() + { + Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, + Action = IdentityServerSecurityLogActionConsts.LoginInvalidUserName + }); } context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, errorDescription); diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/SignInResultExtensions.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/SignInResultExtensions.cs new file mode 100644 index 0000000000..6f167912c3 --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/SignInResultExtensions.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Identity; + +namespace Volo.Abp.IdentityServer.AspNetIdentity +{ + public static class SignInResultExtensions + { + public static string ToIdentitySecurityLogAction(this SignInResult result) + { + if (result.Succeeded) + { + return IdentityServerSecurityLogActionConsts.LoginSucceeded; + } + + if (result.IsLockedOut) + { + return IdentityServerSecurityLogActionConsts.LoginLockedout; + } + + if (result.RequiresTwoFactor) + { + return IdentityServerSecurityLogActionConsts.LoginRequiresTwoFactor; + } + + if (result.IsNotAllowed) + { + return IdentityServerSecurityLogActionConsts.LoginNotAllowed; + } + + if (!result.Succeeded) + { + return IdentityServerSecurityLogActionConsts.LoginFailed; + } + + return IdentityServerSecurityLogActionConsts.LoginFailed; + } + } +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDataSeedContributor.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDataSeedContributor.cs index 906192fd4f..ac8274d445 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDataSeedContributor.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDataSeedContributor.cs @@ -30,6 +30,7 @@ namespace Volo.Abp.PermissionManagement var permissionNames = PermissionDefinitionManager .GetPermissions() .Where(p => p.MultiTenancySide.HasFlag(multiTenancySide)) + .Where(p => !p.Providers.Any() || p.Providers.Contains(RolePermissionValueProvider.ProviderName)) .Select(p => p.Name) .ToArray(); diff --git a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDataSeedContributor_Tests.cs b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDataSeedContributor_Tests.cs new file mode 100644 index 0000000000..a888beb5f9 --- /dev/null +++ b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDataSeedContributor_Tests.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Data; +using Xunit; + +namespace Volo.Abp.PermissionManagement +{ + public class PermissionDataSeedContributor_Tests : PermissionTestBase + { + private readonly PermissionDataSeedContributor _permissionDataSeedContributor; + private readonly IPermissionGrantRepository _grantpermissionGrantRepository; + + public PermissionDataSeedContributor_Tests() + { + _permissionDataSeedContributor = GetRequiredService(); + _grantpermissionGrantRepository = GetRequiredService(); + } + + [Fact] + public async Task SeedAsync() + { + (await _grantpermissionGrantRepository.FindAsync("MyPermission1", RolePermissionValueProvider.ProviderName, "admin")).ShouldBeNull(); + (await _grantpermissionGrantRepository.FindAsync("MyPermission4", RolePermissionValueProvider.ProviderName, "admin")).ShouldBeNull(); + + await _permissionDataSeedContributor.SeedAsync(new DataSeedContext(null)); + + (await _grantpermissionGrantRepository.FindAsync("MyPermission1", RolePermissionValueProvider.ProviderName, "admin")).ShouldNotBeNull(); + (await _grantpermissionGrantRepository.FindAsync("MyPermission4", RolePermissionValueProvider.ProviderName, "admin")).ShouldBeNull(); + } + } +} diff --git a/modules/permission-management/test/Volo.Abp.PermissionManagement.TestBase/Volo/Abp/PermissionManagement/TestPermissionDefinitionProvider.cs b/modules/permission-management/test/Volo.Abp.PermissionManagement.TestBase/Volo/Abp/PermissionManagement/TestPermissionDefinitionProvider.cs index 584c4dd2be..c6de42ac3b 100644 --- a/modules/permission-management/test/Volo.Abp.PermissionManagement.TestBase/Volo/Abp/PermissionManagement/TestPermissionDefinitionProvider.cs +++ b/modules/permission-management/test/Volo.Abp.PermissionManagement.TestBase/Volo/Abp/PermissionManagement/TestPermissionDefinitionProvider.cs @@ -16,6 +16,8 @@ namespace Volo.Abp.PermissionManagement myPermission2.AddChild("MyPermission2.ChildPermission1"); testGroup.AddPermission("MyPermission3", multiTenancySide: MultiTenancySides.Host); + + testGroup.AddPermission("MyPermission4", multiTenancySide: MultiTenancySides.Host).WithProviders(UserPermissionValueProvider.ProviderName); } } -} \ No newline at end of file +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs index 617d360898..a568184f0e 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs @@ -5,6 +5,7 @@ using Volo.Abp.Caching; using Volo.Abp.DependencyInjection; using Volo.Abp.Guids; using Volo.Abp.Settings; +using Volo.Abp.Uow; namespace Volo.Abp.SettingManagement { @@ -16,8 +17,8 @@ namespace Volo.Abp.SettingManagement protected IGuidGenerator GuidGenerator { get; } public SettingManagementStore( - ISettingRepository settingRepository, - IGuidGenerator guidGenerator, + ISettingRepository settingRepository, + IGuidGenerator guidGenerator, IDistributedCache cache, ISettingDefinitionManager settingDefinitionManager) { @@ -27,11 +28,13 @@ namespace Volo.Abp.SettingManagement SettingDefinitionManager = settingDefinitionManager; } + [UnitOfWork] public virtual async Task GetOrNullAsync(string name, string providerName, string providerKey) { return (await GetCacheItemAsync(name, providerName, providerKey)).Value; } + [UnitOfWork] public virtual async Task SetAsync(string name, string value, string providerName, string providerKey) { var setting = await SettingRepository.FindAsync(name, providerName, providerKey); @@ -45,6 +48,8 @@ namespace Volo.Abp.SettingManagement setting.Value = value; await SettingRepository.UpdateAsync(setting); } + + await Cache.SetAsync(CalculateCacheKey(name, providerName, providerKey), new SettingCacheItem(setting?.Value), considerUow: true); } public virtual async Task> GetListAsync(string providerName, string providerKey) @@ -53,19 +58,21 @@ namespace Volo.Abp.SettingManagement return settings.Select(s => new SettingValue(s.Name, s.Value)).ToList(); } + [UnitOfWork] public virtual async Task DeleteAsync(string name, string providerName, string providerKey) { var setting = await SettingRepository.FindAsync(name, providerName, providerKey); if (setting != null) { await SettingRepository.DeleteAsync(setting); + await Cache.RemoveAsync(CalculateCacheKey(name, providerName, providerKey), considerUow: true); } } protected virtual async Task GetCacheItemAsync(string name, string providerName, string providerKey) { var cacheKey = CalculateCacheKey(name, providerName, providerKey); - var cacheItem = await Cache.GetAsync(cacheKey); + var cacheItem = await Cache.GetAsync(cacheKey, considerUow: true); if (cacheItem != null) { @@ -75,26 +82,26 @@ namespace Volo.Abp.SettingManagement cacheItem = new SettingCacheItem(null); await SetCacheItemsAsync(providerName, providerKey, name, cacheItem); - + return cacheItem; } private async Task SetCacheItemsAsync( - string providerName, - string providerKey, - string currentName, + string providerName, + string providerKey, + string currentName, SettingCacheItem currentCacheItem) { var settingDefinitions = SettingDefinitionManager.GetAll(); var settingsDictionary = (await SettingRepository.GetListAsync(providerName, providerKey)) .ToDictionary(s => s.Name, s => s.Value); - - var cacheItems = new List>(); - + + var cacheItems = new List>(); + foreach (var settingDefinition in settingDefinitions) { var settingValue = settingsDictionary.GetOrDefault(settingDefinition.Name); - + cacheItems.Add( new KeyValuePair( CalculateCacheKey(settingDefinition.Name, providerName, providerKey), @@ -108,7 +115,7 @@ namespace Volo.Abp.SettingManagement } } - await Cache.SetManyAsync(cacheItems); + await Cache.SetManyAsync(cacheItems, considerUow: true); } protected virtual string CalculateCacheKey(string name, string providerName, string providerKey) diff --git a/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManagementStore_Tests.cs b/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManagementStore_Tests.cs index 753ba15054..7a405faf2d 100644 --- a/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManagementStore_Tests.cs +++ b/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManagementStore_Tests.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using Shouldly; using Volo.Abp.Settings; +using Volo.Abp.Uow; using Xunit; namespace Volo.Abp.SettingManagement @@ -13,12 +14,14 @@ namespace Volo.Abp.SettingManagement private readonly ISettingManagementStore _settingManagementStore; private readonly ISettingRepository _settingRepository; private readonly SettingTestData _testData; + private readonly IUnitOfWorkManager _unitOfWorkManager; public SettingManagementStore_Tests() { _settingManagementStore = GetRequiredService(); _settingRepository = GetRequiredService(); _testData = GetRequiredService(); + _unitOfWorkManager= GetRequiredService(); } [Fact] @@ -50,6 +53,21 @@ namespace Volo.Abp.SettingManagement (await _settingRepository.FindAsync(_testData.SettingId)).Value.ShouldBe("43"); } + [Fact] + public async Task Set_In_UnitOfWork_Should_Be_Consistent() + { + using (_unitOfWorkManager.Begin()) + { + var value = await _settingManagementStore.GetOrNullAsync("MySetting1", GlobalSettingValueProvider.ProviderName, null); + value.ShouldBe("42"); + + await _settingManagementStore.SetAsync("MySetting1", "43", GlobalSettingValueProvider.ProviderName, null); + + var valueAfterSet = await _settingManagementStore.GetOrNullAsync("MySetting1", GlobalSettingValueProvider.ProviderName, null); + valueAfterSet.ShouldBe("43"); + } + } + [Fact] public async Task DeleteAsync() { @@ -60,5 +78,21 @@ namespace Volo.Abp.SettingManagement (await _settingRepository.FindAsync(_testData.SettingId)).ShouldBeNull(); } + [Fact] + public async Task Delete_In_UnitOfWork_Should_Be_Consistent() + { + using (var uow = _unitOfWorkManager.Begin()) + { + (await _settingManagementStore.GetOrNullAsync("MySetting1", GlobalSettingValueProvider.ProviderName, null)).ShouldNotBeNull(); + + await _settingManagementStore.DeleteAsync("MySetting1", GlobalSettingValueProvider.ProviderName, null); + + await uow.SaveChangesAsync(); + + var value = await _settingManagementStore.GetOrNullAsync("MySetting1", GlobalSettingValueProvider.ProviderName, null); + value.ShouldBeNull(); + } + } + } } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js index b168fe0fb6..4b19d0bbb7 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js @@ -17,69 +17,79 @@ modalClass: 'TenantConnectionStringManagement', }); - abp.ui.extensions.tableColumns.get("tenant").addContributor( + var _dataTable = null; + + abp.ui.extensions.entityActions.get('tenantManagement.tenant').addContributor( + function(actionList) { + return actionList.addManyTail( + [ + { + text: l('Edit'), + visible: abp.auth.isGranted( + 'AbpTenantManagement.Tenants.Update' + ), + action: function (data) { + _editModal.open({ + id: data.record.id, + }); + }, + }, + { + text: l('ConnectionStrings'), + visible: abp.auth.isGranted( + 'AbpTenantManagement.Tenants.ManageConnectionStrings' + ), + action: function (data) { + _connectionStringsModal.open({ + id: data.record.id, + }); + }, + }, + { + text: l('Features'), + visible: abp.auth.isGranted( + 'AbpTenantManagement.Tenants.ManageFeatures' + ), + action: function (data) { + _featuresModal.open({ + providerName: 'T', + providerKey: data.record.id, + }); + }, + }, + { + text: l('Delete'), + visible: abp.auth.isGranted( + 'AbpTenantManagement.Tenants.Delete' + ), + confirmMessage: function (data) { + return l( + 'TenantDeletionConfirmationMessage', + data.record.name + ); + }, + action: function (data) { + _tenantAppService + .delete(data.record.id) + .then(function () { + _dataTable.ajax.reload(); + }); + }, + } + ] + ); + } + ); + + abp.ui.extensions.tableColumns.get('tenantManagement.tenant').addContributor( function (columnList) { columnList.addManyTail( [ { title: l("Actions"), rowAction: { - items: [ - { - text: l('Edit'), - visible: abp.auth.isGranted( - 'AbpTenantManagement.Tenants.Update' - ), - action: function (data) { - _editModal.open({ - id: data.record.id, - }); - }, - }, - { - text: l('ConnectionStrings'), - visible: abp.auth.isGranted( - 'AbpTenantManagement.Tenants.ManageConnectionStrings' - ), - action: function (data) { - _connectionStringsModal.open({ - id: data.record.id, - }); - }, - }, - { - text: l('Features'), - visible: abp.auth.isGranted( - 'AbpTenantManagement.Tenants.ManageFeatures' - ), - action: function (data) { - _featuresModal.open({ - providerName: 'T', - providerKey: data.record.id, - }); - }, - }, - { - text: l('Delete'), - visible: abp.auth.isGranted( - 'AbpTenantManagement.Tenants.Delete' - ), - confirmMessage: function (data) { - return l( - 'TenantDeletionConfirmationMessage', - data.record.name - ); - }, - action: function (data) { - _tenantAppService - .delete(data.record.id) - .then(function () { - _dataTable.ajax.reload(); - }); - }, - }, - ], - }, + items: abp.ui.extensions.entityActions.get('tenantManagement.tenant').actions.toArray() + } }, { title: l("TenantName"), @@ -94,7 +104,7 @@ $(function () { var _$wrapper = $('#TenantsWrapper'); - var _dataTable = _$wrapper.find('table').DataTable( + _dataTable = _$wrapper.find('table').DataTable( abp.libs.datatables.normalizeConfiguration({ order: [[1, 'asc']], processing: true, @@ -102,7 +112,7 @@ scrollX: true, serverSide: true, ajax: abp.libs.datatables.createAjax(_tenantAppService.getList), - columnDefs: abp.ui.extensions.tableColumns.get("tenant").columns.toArray(), + columnDefs: abp.ui.extensions.tableColumns.get('tenantManagement.tenant').columns.toArray(), }) ); diff --git a/npm/ng-packs/packages/core/src/lib/core.module.ts b/npm/ng-packs/packages/core/src/lib/core.module.ts index f118ab2836..3fd29fec53 100644 --- a/npm/ng-packs/packages/core/src/lib/core.module.ts +++ b/npm/ng-packs/packages/core/src/lib/core.module.ts @@ -34,7 +34,7 @@ import { ConfigState } from './states/config.state'; import { ProfileState } from './states/profile.state'; import { ReplaceableComponentsState } from './states/replaceable-components.state'; import { SessionState } from './states/session.state'; -import { CORE_OPTIONS } from './tokens/options.token'; +import { CORE_OPTIONS, coreOptionsFactory } from './tokens/options.token'; import { noop } from './utils/common-utils'; import './utils/date-extensions'; import { getInitialData, localeInitializer, configureOAuth } from './utils/initial-utils'; @@ -171,9 +171,14 @@ export class CoreModule { useValue: { environment: options.environment }, }, { - provide: CORE_OPTIONS, + provide: 'CORE_OPTIONS', useValue: options, }, + { + provide: CORE_OPTIONS, + useFactory: coreOptionsFactory, + deps: ['CORE_OPTIONS'], + }, { provide: HTTP_INTERCEPTORS, useClass: ApiInterceptor, diff --git a/npm/ng-packs/packages/core/src/lib/models/common.ts b/npm/ng-packs/packages/core/src/lib/models/common.ts index 930265020c..aa2f961b7b 100644 --- a/npm/ng-packs/packages/core/src/lib/models/common.ts +++ b/npm/ng-packs/packages/core/src/lib/models/common.ts @@ -9,6 +9,7 @@ export namespace ABP { environment: Partial; skipGetAppConfiguration?: boolean; sendNullsAsQueryParam?: boolean; + cultureNameToLocaleFileNameMapping?: Dictionary; } export interface Test { diff --git a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts index 3f89642fd9..e488bafaf4 100644 --- a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts @@ -8,6 +8,7 @@ import { Config } from '../models/config'; import { ConfigState } from '../states/config.state'; import { registerLocale } from '../utils/initial-utils'; import { createLocalizer, createLocalizerWithFallback } from '../utils/localization-utils'; +import { CORE_OPTIONS } from '../tokens/options.token'; type ShouldReuseRoute = (future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot) => boolean; @@ -44,11 +45,12 @@ export class LocalizationService { registerLocale(locale: string) { const router = this.injector.get(Router); + const { cultureNameToLocaleFileNameMapping: localeNameMap } = this.injector.get(CORE_OPTIONS); const { shouldReuseRoute } = router.routeReuseStrategy; router.routeReuseStrategy.shouldReuseRoute = () => false; router.navigated = false; - return registerLocale(locale).then(() => { + return registerLocale(locale, localeNameMap).then(() => { this.ngZone.run(async () => { await router.navigateByUrl(router.url).catch(noop); router.routeReuseStrategy.shouldReuseRoute = shouldReuseRoute; diff --git a/npm/ng-packs/packages/core/src/lib/tokens/options.token.ts b/npm/ng-packs/packages/core/src/lib/tokens/options.token.ts index 2c4dffe9dc..a4b05c8cc7 100644 --- a/npm/ng-packs/packages/core/src/lib/tokens/options.token.ts +++ b/npm/ng-packs/packages/core/src/lib/tokens/options.token.ts @@ -1,4 +1,15 @@ import { InjectionToken } from '@angular/core'; import { ABP } from '../models/common'; +import differentLocales from '../constants/different-locales'; export const CORE_OPTIONS = new InjectionToken('CORE_OPTIONS'); + +export function coreOptionsFactory({ + cultureNameToLocaleFileNameMapping: localeNameMap = {}, + ...options +}: ABP.Root) { + return { + ...options, + cultureNameToLocaleFileNameMapping: { ...differentLocales, ...localeNameMap }, + } as ABP.Root; +} diff --git a/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts index bfca50bcd2..78677b7ca4 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts @@ -4,7 +4,6 @@ import { Store } from '@ngxs/store'; import { OAuthService } from 'angular-oauth2-oidc'; import { tap } from 'rxjs/operators'; import { GetAppConfiguration } from '../actions/config.actions'; -import differentLocales from '../constants/different-locales'; import { ABP } from '../models/common'; import { ConfigState } from '../states/config.state'; import { CORE_OPTIONS } from '../tokens/options.token'; @@ -45,22 +44,25 @@ function checkAccessToken(store: Store, injector: Injector) { export function localeInitializer(injector: Injector) { const fn = () => { const store: Store = injector.get(Store); + const options = injector.get(CORE_OPTIONS); const lang = store.selectSnapshot(state => state.SessionState.language) || 'en'; return new Promise((resolve, reject) => { - registerLocale(lang).then(() => resolve('resolved'), reject); + registerLocale(lang, options.cultureNameToLocaleFileNameMapping).then( + () => resolve('resolved'), + reject, + ); }); }; return fn; } -export function registerLocale(locale: string) { +export function registerLocale(locale: string, localeNameMap: ABP.Dictionary) { return import( - /* webpackInclude: /(af|ar|am|ar-SA|as|az-Latn|be|bg|bn-BD|bn-IN|bs|ca|ca-ES-VALENCIA|cs|cy|da|de|de|el|en-GB|en|es|en|es-US|es-MX|et|eu|fa|fi|en|fr|fr|fr-CA|ga|gd|gl|gu|ha|he|hi|hr|hu|hy|id|ig|is|it|it|ja|ka|kk|km|kn|ko|kok|en|en|lb|lt|lv|en|mk|ml|mn|mr|ms|mt|nb|ne|nl|nl-BE|nn|en|or|pa|pa-Arab|pl|en|pt|pt-PT|en|en|ro|ru|rw|pa-Arab|si|sk|sl|sq|sr-Cyrl-BA|sr-Cyrl|sr-Latn|sv|sw|ta|te|tg|th|ti|tk|tn|tr|tt|ug|uk|ur|uz-Latn|vi|wo|xh|yo|zh-Hans|zh-Hant|zu)\.js$/ */ - /* webpackChunkName: "[request]"*/ - `@angular/common/locales/${differentLocales[locale] || locale}.js` + /* webpackChunkName: "_locale-[request]"*/ + `@angular/common/locales/${localeNameMap[locale] || locale}.js` ).then(module => { registerLocaleData(module.default); }); diff --git a/npm/ng-packs/packages/theme-basic/src/lib/theme-basic.module.ts b/npm/ng-packs/packages/theme-basic/src/lib/theme-basic.module.ts index f599124e83..bfacbcf9a7 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/theme-basic.module.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/theme-basic.module.ts @@ -42,6 +42,20 @@ export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, Empt NgbCollapseModule, NgbDropdownModule, NgxValidateCoreModule, + ], + entryComponents: [...LAYOUTS, ValidationErrorComponent, CurrentUserComponent, LanguagesComponent], +}) +export class ThemeBasicModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: RootThemeBasicModule, + providers: [BASIC_THEME_NAV_ITEM_PROVIDERS, BASIC_THEME_STYLES_PROVIDERS], + }; + } +} + +@NgModule({ + imports: [ NgxValidateCoreModule.forRoot({ targetSelector: '.form-group', blueprints: { @@ -63,13 +77,5 @@ export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, Empt errorTemplate: ValidationErrorComponent, }), ], - entryComponents: [...LAYOUTS, ValidationErrorComponent, CurrentUserComponent, LanguagesComponent], }) -export class ThemeBasicModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: ThemeBasicModule, - providers: [BASIC_THEME_NAV_ITEM_PROVIDERS, BASIC_THEME_STYLES_PROVIDERS], - }; - } -} +export class RootThemeBasicModule {} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application.Contracts/MyCompanyName.MyProjectName.Application.Contracts.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application.Contracts/MyCompanyName.MyProjectName.Application.Contracts.csproj index f9c3f9a045..fe2e40eb14 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application.Contracts/MyCompanyName.MyProjectName.Application.Contracts.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application.Contracts/MyCompanyName.MyProjectName.Application.Contracts.csproj @@ -13,7 +13,6 @@ - diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application.Contracts/MyProjectNameApplicationContractsModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application.Contracts/MyProjectNameApplicationContractsModule.cs index d8e848e86b..5c3ba41364 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application.Contracts/MyProjectNameApplicationContractsModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application.Contracts/MyProjectNameApplicationContractsModule.cs @@ -1,5 +1,4 @@ using Volo.Abp.Account; -using Volo.Abp.AutoMapper; using Volo.Abp.FeatureManagement; using Volo.Abp.Identity; using Volo.Abp.Modularity; @@ -16,8 +15,7 @@ namespace MyCompanyName.MyProjectName typeof(AbpIdentityApplicationContractsModule), typeof(AbpPermissionManagementApplicationContractsModule), typeof(AbpTenantManagementApplicationContractsModule), - typeof(AbpObjectExtendingModule), - typeof(AbpAutoMapperModule) + typeof(AbpObjectExtendingModule) )] public class MyProjectNameApplicationContractsModule : AbpModule { @@ -25,13 +23,5 @@ namespace MyCompanyName.MyProjectName { MyProjectNameDtoExtensions.Configure(); } - - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.AddMaps(); - }); - } } }