diff --git a/README.md b/README.md index 1d7b7e51a7..ebba20da41 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Love ABP Framework? **Please give a star** to this repository :star: ## Discord Channel -You can use this link to join the ABP Community Discord Server: https://discord.gg/uVGt6hyhcm +You can use this link to join the ABP Community Discord Server: https://discord.gg/abp ## ABP Commercial diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 023a4b18ed..18c5524d1f 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -380,6 +380,8 @@ "PurchaseItems": "Purchase Items", "SuccessfullyUpdated": "Successfully updated", "SuccessfullyAdded": "Successfully added", - "PurchaseState": "Purchase State" + "PurchaseState": "Purchase State", + "ShowBetweenDayCount": "Show Between Days", + "PurchaseOrder": "Purchase Order" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json index fe0947da89..5d6e081085 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json @@ -84,7 +84,7 @@ "WouldLikeToReceiveMarketingMaterials": "I would like to receive marketing materials like product deals & special offers.", "JoinOurMarketingNewsletter": "Join our marketing newsletter", "CommunityPrivacyPolicyConfirmation": "I agree to the Terms & Conditions and Privacy Policy.", - "ABPIO-Common": "ABPIO-Common", + "WouldLikeToReceiveNotification": "I would like to receive the latest news from abp.io websites.", "CommercialNewsletterConfirmationMessage": "I agree to the Terms & Conditions and Privacy Policy.", "FreeDDDEBook": "Free DDD E-Book", "AdditionalServices": "Additional Services", @@ -118,6 +118,9 @@ "ThereIsNoEvent": "There is no event.", "Events": "Events", "Volo.AbpIo.Domain:080000": "There is already a purchase item named \"{Name}\"", - "MasteringAbpFrameworkBook": "Book: Mastering ABP Framework" + "MasteringAbpFrameworkBook": "Book: Mastering ABP Framework", + "ABPIO-CommonPreferenceDefinition": "Get the latest news about ABP Platform like new posts, events and more.", + "BuiltOn": "Built-on", + "AbpFramework": "ABP Framework" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index 8fa96b5ee2..0d7724df56 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -495,7 +495,9 @@ "LicenseTypeNotCorrect": "The license type is not correct!", "Trainings": "Trainings", "ChoseTrainingPlaceholder": "Chose the training...", - "ContactUsToGetQuote": "Contact us to get a quote", + "DoYouNeedTrainings": "Do you need one of these trainings?", + "DoYouNeedTraining": "Do you need {0} training?", + "GetInTouchUs": "Get in touch with us", "ForMoreInformationClickHere": "For more information, click here.", "IsGetOnboardingTraining": "Would you like to get onboarding & web application development training?", "OnboardingWebApplicationDevelopmentTrainingMessage": "To schedule your training calendar, please contact {0} after creating the organization", @@ -503,6 +505,7 @@ "AdditionalNote": "Additional Note", "OnboardingTrainingFaqTitle": "Do you have ABP onboarding training?", "OnboardingTrainingFaqExplanation": "Yes, we have ABP Training Services to help you get your ABP project started fast. You will learn about ABP from an ABP core team member and you will get the skills to begin your ABP project. In the onboarding training, we will explain how to set up your development environment, install the required tools, create a fully functional CRUD page. The training will be live and the Zoom application will be used, and we are open to using other online meeting platforms. The language of the training will be English. You can also ask your questions about ABP during the sessions. A convenient time and date will be planned for both parties. To get more information, contact us at info@abp.io.", - "AddBasket": "Add to Basket" + "AddBasket": "Add to Basket", + "SendTrainingRequest": "Send Training Request" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index 550374b48c..0b9d6d96b2 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -183,7 +183,7 @@ "InstallABPCLIInfo": "ABP CLI is the fastest way to start a new solution with the ABP framework. Install the ABP CLI using a command line window:", "DifferentLevelOfNamespaces": "You can use different levels of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.", "ABPCLIExamplesInfo": "The new command creates a layered MVC application with Entity Framework Core as the database provider. However, it has additional options.", - "SeeCliDocumentForMoreInformation": "Check out the ABP CLI document for more options or select the \"Direct Download\" tab above.", + "SeeCliDocumentForMoreInformation": "Check out the ABP CLI document for more options or select the \"Direct Download\" tab above.", "Optional": "Optional", "LocalFrameworkRef": "Keep the local project reference for the framework packages.", "BlobStoring": "BLOB Storing", diff --git a/docs/en/API/API-Versioning.md b/docs/en/API/API-Versioning.md index 5eefb129c3..68343417de 100644 --- a/docs/en/API/API-Versioning.md +++ b/docs/en/API/API-Versioning.md @@ -26,7 +26,7 @@ public override void ConfigureServices(ServiceConfigurationContext context) ## C# and JavaScript Static Client Proxies -This feature does not compatible with [URL Path Versioning](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-URL-Path), I suggest you always use [Versioning-via-the-Query-String](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-Query-String). +This feature does not compatible with [URL Path Versioning](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-URL-Path), we suggest to use [Versioning-via-the-Query-String](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-Query-String). ### Example @@ -45,7 +45,7 @@ public interface IBookV2AppService : IApplicationService } ``` -**HttpApi Controillers:** +**HttpApi Controllers:** ```cs [Area(BookStoreRemoteServiceConsts.ModuleName)] [RemoteService(Name = BookStoreRemoteServiceConsts.RemoteServiceName)] @@ -175,7 +175,7 @@ bookStore.books.bookV2.getAsyncByIsbn = function(isbn, api_version, ajaxParams) ``` -## Manually change version +## Changing version manually If an application service class supports multiple versions. You can inject `ICurrentApiVersionInfo` to switch versions in C#. @@ -337,13 +337,15 @@ public override void OnApplicationInitialization(ApplicationInitializationContex } ``` -## Custom multi-version API controller. +## Custom multi-version API controller -ABP Framework will not affect to your APIs, you can freely implement your APIs according to Microsoft's documentation. +ABP Framework will not affect to your APIs, you can freely implement your APIs according to the Microsoft's documentation. -Get more from https://github.com/dotnet/aspnet-api-versioning/wiki +Further information, see https://github.com/dotnet/aspnet-api-versioning/wiki ## Sample source code -You can get the complete sample source code in https://github.com/abpframework/abp-samples/tree/master/Api-Versioning \ No newline at end of file +Follow the link below to get the sample's complete source-code + +https://github.com/abpframework/abp-samples/tree/master/Api-Versioning diff --git a/docs/en/AspNet-Boilerplate-Migration-Guide.md b/docs/en/AspNet-Boilerplate-Migration-Guide.md index 52a2fced6a..e337b78a6d 100644 --- a/docs/en/AspNet-Boilerplate-Migration-Guide.md +++ b/docs/en/AspNet-Boilerplate-Migration-Guide.md @@ -1,4 +1,4 @@ -# ASP.NET Boilerplate v5+ to ABP Framework Migration +# Migrating from ASP.NET Boilerplate to the ABP Framework ABP Framework is **the successor** of the open source [ASP.NET Boilerplate](https://aspnetboilerplate.com/) framework. This guide aims to help you to **migrate your existing solutions** (you developed with the ASP.NET Boilerplate framework) to the ABP Framework. diff --git a/docs/en/Background-Workers-Hangfire.md b/docs/en/Background-Workers-Hangfire.md index 7f832f2c71..4cbaf8cc24 100644 --- a/docs/en/Background-Workers-Hangfire.md +++ b/docs/en/Background-Workers-Hangfire.md @@ -126,4 +126,4 @@ context.ServiceProvider So, it resolves the given background worker and adds to the `IBackgroundWorkerManager`. -While we generally add workers in OnApplicationInitialization, there are no restrictions on that. You can inject IBackgroundWorkerManager anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down. \ No newline at end of file +While we generally add workers in OnApplicationInitialization, there are no restrictions on that. You can inject IBackgroundWorkerManager anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down. diff --git a/docs/en/Background-Workers.md b/docs/en/Background-Workers.md index 0c9e26e409..726a8817ab 100644 --- a/docs/en/Background-Workers.md +++ b/docs/en/Background-Workers.md @@ -86,20 +86,20 @@ After creating a background worker class, you should add it to the `IBackgroundW [DependsOn(typeof(AbpBackgroundWorkersModule))] public class MyModule : AbpModule { - public override void OnApplicationInitialization( + public override Task OnApplicationInitializationAsync( ApplicationInitializationContext context) { - context.AddBackgroundWorker(); + context.AddBackgroundWorkerAsync(); } } ```` -`context.AddBackgroundWorker(...)` is a shortcut extension method for the expression below: +`context.AddBackgroundWorkerAsync(...)` is a shortcut extension method for the expression below: ````csharp -context.ServiceProvider +await context.ServiceProvider .GetRequiredService() - .Add( + .AddAsync( context .ServiceProvider .GetRequiredService() diff --git a/docs/en/Blog-Posts/2022-04-05 v5_2_Release_Stable/POST.md b/docs/en/Blog-Posts/2022-04-05 v5_2_Release_Stable/POST.md new file mode 100644 index 0000000000..ed83f36e19 --- /dev/null +++ b/docs/en/Blog-Posts/2022-04-05 v5_2_Release_Stable/POST.md @@ -0,0 +1,51 @@ +# ABP.IO Platform 5.2 Final Has Been Released! + +[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 5.2 versions have been released today. + +## What's New With 5.2? + +Since all the new features are already explained in details with the [5.2 RC Announcement Post](https://blog.abp.io/abp/ABP.IO-Platform-5-2-RC-Has-Been-Published), I will not repeat all the details again. See the [RC Blog Post](https://blog.abp.io/abp/ABP.IO-Platform-5-2-RC-Has-Been-Published) for all the features and enhancements. + +## Creating New Solutions + +You can create a new solution with the ABP Framework version 5.2 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started). + +> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for more. + +## How to Upgrade an Existing Solution + +### Install/Update the ABP CLI + +First of all, install the ABP CLI or upgrade to the latest version. + +If you haven't installed yet: + +```bash +dotnet tool install -g Volo.Abp.Cli +``` + +To update an existing installation: + +```bash +dotnet tool update -g Volo.Abp.Cli +``` + +### ABP UPDATE Command + +[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides a handy command to update all the ABP related NuGet and NPM packages in your solution with a single command: + +```bash +abp update +``` + +Run this command in the root folder of your solution. + +## Migration Guide + +Check [the migration guide](https://docs.abp.io/en/abp/5.2/Migration-Guides/Abp-5_2) for the applications with the version 5.x upgrading to the version 5.2. + +## About the Next Version + +The next feature version will be 5.3. It is planned to release the 5.3 RC (Release Candidate) on May 03 and the final version on May 31, 2022. You can follow the [release planning here](https://github.com/abpframework/abp/milestones). + +Please [submit an issue](https://github.com/abpframework/abp/issues/new) if you have any problem with this version. diff --git a/docs/en/Caching.md b/docs/en/Caching.md index 13129bbc95..074d6d951f 100644 --- a/docs/en/Caching.md +++ b/docs/en/Caching.md @@ -258,7 +258,7 @@ ABP's distributed cache interfaces provide methods to perform batch methods thos Distributed cache service provides an interesting feature. Assume that you've updated the price of a book in the database, then set the new price to the cache, so you can use the cached value later. What if you have an exception after setting the cache and you **rollback the transaction** that updates the price of the book? In this case, cache value will be incorrect. -`IDistributedCache<..>` methods gets an optional parameter, named `considerOuw`, which is `false` by default. If you set it to `true`, then the changes you made for the cache are not actually applied to the real cache store, but associated with the current [unit of work](Unit-Of-Work.md). You get the value you set in the same unit of work, but the changes are applied **only if the current unit of work succeed**. +`IDistributedCache<..>` methods gets an optional parameter, named `considerUow`, which is `false` by default. If you set it to `true`, then the changes you made for the cache are not actually applied to the real cache store, but associated with the current [unit of work](Unit-Of-Work.md). You get the value you set in the same unit of work, but the changes are applied **only if the current unit of work succeed**. ### IDistributedCacheSerializer diff --git a/docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/POST.md b/docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/POST.md new file mode 100644 index 0000000000..b16d6bd9f1 --- /dev/null +++ b/docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/POST.md @@ -0,0 +1,548 @@ +# Handle Concurrency with EF Core in an ABP Framework Project with ASP.NET Core MVC + +In this article, we'll create a basic application to demonstrate how "Concurrency Check/Control" can be implemented in an ABP project. + +## Creating the Solution + +For this article, we will create a simple BookStore application and add CRUD functionality to the pages. Hence we deal with the concurrency situation. + +We can create a new startup template with EF Core as a database provider and MVC for the UI Framework. + +> If you already have a project, you don't need to create a new startup template, you can directly implement the following steps to your project. So you can skip this section. + +We can create a new startup template by using the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI). + +```bash +abp new Acme.BookStore +``` + +After running the above command, our project boilerplate will be downloaded. Then we can open the solution and start the development. + +## Starting the Development + +Let's start with defining our entities. + +### Creating Entities + +Create a `Book.cs` (/Books/Book.cs) class in the `.Domain` layer: + +```csharp +public class Book : AuditedAggregateRoot +{ + public string Name { get; set; } + + public BookType Type { get; set; } + + public DateTime PublishDate { get; set; } + + public float Price { get; set; } +} +``` + +* To enable **Concurrency Check** for our entities, our entities should be implemented the `IHasConcurrencyStamp` interface, directly or indirectly. + +* [Aggregate Root](https://docs.abp.io/en/abp/5.2/Entities#aggregateroot-class) entity classes already implement the `IHasConcurrencyStamp` interface, so if we inherit our entities from one of these entity classes then we won't need to manually implement the `IHasConcurrencyStamp` interface. + +* And we've derived the `Book` entity from `AuditedAggregateRoot` here, so we don't need to implement the `IHasConcurrencyStamp` interface because `AuditedAggregateRoot` class already implemented the `IHasConcurrencyStamp` interface. + +> You can read more details from the [Concurrency Check](https://docs.abp.io/en/abp/5.2/Concurrency-Check) documentation. + +Then, create a `BookType` (/Books/BookType.cs) enum in the `.Domain.Shared` layer: + +```csharp +public enum BookType +{ + Undefined, + Adventure, + Biography, + Dystopia, + Fantastic, + Horror, + Science, + ScienceFiction, + Poetry +} +``` + +### Database Integration + +Open the `BookStoreDbContext` (/EntityFrameworkCore/BookStoreDbContext.cs) class in the `*.EntityFrameworkCore` project and add the following `DbSet` statement: + +```csharp +namespace Acme.BookStore.EntityFrameworkCore; + +[ReplaceDbContext(typeof(IIdentityDbContext))] +[ReplaceDbContext(typeof(ITenantManagementDbContext))] +[ConnectionStringName("Default")] +public class BookStoreDbContext : + AbpDbContext, + IIdentityDbContext, + ITenantManagementDbContext +{ + //Entities from the modules + + public DbSet Books { get; set; } //add this line +} +``` + +Then we can navigate to the `OnModelCreating` method in the same class and configure our tables/entities: + +```csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + builder.ConfigurePermissionManagement(); + ... + + //* Configure your own tables/entities inside here */ + + 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); + }); +} +``` + +After the mapping configurations, we can create a new migration and apply changes to the database. + +To do this, open your command line terminal in the directory of the `EntityFrameworkCore` project and run the below command: + +```bash +dotnet ef migrations add Added_Books +``` + +After this command, a new migration will be generated and then we can run the `*.DbMigrator` project to apply the last changes to the database such as creating a new table named `Books` according to the last created migration. + +### Defining DTOs and Application Service Interfaces + +We can start to define the use cases of the application. + +Create the DTO classes (under the **Books** folder) in the `Application.Contracts` project: + +**BookDto.cs** + +```csharp +public class BookDto : AuditedEntityDto, IHasConcurrencyStamp +{ + public string Name { get; set; } + + public BookType Type { get; set; } + + public DateTime PublishDate { get; set; } + + public float Price { get; set; } + + public string ConcurrencyStamp { get; set; } +} +``` + +* The `AuditedEntityDto` class is not implemented from the `IHasConcurrencyStamp` interface, so for the **BookDto** class we need to implement the `IHasConcurrencyStamp`. + +* This is important, because we need to return books with their **ConcurrencyStamp** value. + +**CreateBookDto.cs** + +```csharp +public class CreateBookDto +{ + [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; } +} +``` + +**UpdateBookDto.cs** + +```csharp +public class UpdateBookDto : IHasConcurrencyStamp +{ + [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; } + + public string ConcurrencyStamp { get; set; } +} +``` + +* Here, we've implemented the `IHasConcurrencyStamp` interface for the **UpdateBookDto** class. + +* We will use this value while updating an existing book. ABP Framework will compare the current book's **ConcurrencyStamp** value with the provided one, if values are matched, this means everything is as it is supposed to be and will update the record. + +* If values are mismatched, then it means the record that we're trying to update is already updated by another user and we need to get the latest changes to be able to make changes on it. + +* Also, in that case, `AbpDbConcurrencyException` will be thrown by the ABP Framework and we can either handle this exception manually or let the ABP Framework handle it on behalf of us and show a user-friendly error message as in the image below. + +![](./optimistic-concurrency.png) + +Create a new `IBookAppService` (/Books/IBookAppService.cs) interface in the `Application.Contracts` project: + +```csharp +public interface IBookAppService : + ICrudAppService +{ +} +``` +* We've implemented the `ICrudAppService` here, because we just need to perform CRUD operations and this interface helps us define common CRUD operation methods. + +### Application Service Implementations + +Create a `BookAppService` (/Books/BookAppService.cs) class inside the `*.Application` project and implement the application service methods, as shown below: + +```csharp +public class BookAppService : + CrudAppService, + IBookAppService +{ + public BookAppService(IRepository repository) + : base(repository) + { + } + + public override async Task UpdateAsync(Guid id, UpdateBookDto input) + { + var book = await Repository.GetAsync(id); + + book.Name = input.Name; + book.Price = input.Price; + book.Type = input.Type; + book.PublishDate = input.PublishDate; + + //set Concurrency Stamp value to the entity + book.ConcurrencyStamp = input.ConcurrencyStamp; + + var updatedBook = await Repository.UpdateAsync(book); + return ObjectMapper.Map(updatedBook); + } +} +``` + +* We've used the `CrudAppService` base class. This class implements all common CRUD operations and if we want to change a method, we can simply override the method and change it to our needs. + +> Normally, you don't need to override the `UpdateAsync` method to do **Concurrency Check**. Because the `UpdateAsync` method of the `CrudAppService` class by default map input values to the entity. But I wanted to override this method to show what we need to do for **Concurrency Check**. + +* We can look closer to the `UpdateAsync` method here, because as we've mentioned earlier we need to pass the provided **ConcurrencyStamp** value to be able to do **Concurrency Check/Control** to our entity while updating. + +* At that point, if the given record is already updated by any other user, a **ConcurrencyStamp** mismatch will occur and `AbpDbConcurrencyException` will be thrown thanks to the **Concurrency Check** system of ABP, data-consistency will be provided and the current record won't be overridden. + +* And if the values are matched, the record will be updated successfully. + +After implementing the application service methods, we can do the related mapping configurations, so open the `BookStoreApplicationAutoMapperProfile.cs` and update the content as below: + +```csharp +public class BookStoreApplicationAutoMapperProfile : Profile +{ + public BookStoreApplicationAutoMapperProfile() + { + CreateMap(); + CreateMap(); + } +} +``` + +### User Interface + +So far, we've applied the all necessary steps for the **Concurrency Check** system, let's see it in action. + +Create a razor page in the `.Web` layer named `Index` (**/Pages/Books/Index.cshtml**), open this file and replace the content with the following code block: + +```html +@page +@using Acme.BookStore.Localization +@using Microsoft.Extensions.Localization +@model Acme.BookStore.Web.Pages.Books.Index + +@section scripts +{ + +} + + + + + + Books + + + + + + + + + + +``` + +* We've defined a table and "New Book" button inside a card element here, we'll fill the table with our book records in the next step by using the **Datatables** library. + +Create an `Index.js` (**/Pages/Books/Index.js**) file and add the following code block: + +```js +$(function () { + var l = abp.localization.getResource('BookStore'); + var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); + var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); + + var dataTable = $('#BooksTable').DataTable( + abp.libs.datatables.normalizeConfiguration({ + serverSide: true, + paging: true, + order: [[1, "asc"]], + searching: false, + scrollX: true, + ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), + columnDefs: [ + { + title: l('Actions'), + rowAction: { + items: + [ + { + text: l('Edit'), + action: function (data) { + editModal.open({ id: data.record.id }); + } + } + ] + } + }, + { + title: l('Name'), + data: "name" + }, + { + title: l('Type'), + data: "type", + render: function (data) { + return l('Enum:BookType:' + data); + } + }, + { + title: l('PublishDate'), + data: "publishDate", + render: function (data) { + return luxon + .DateTime + .fromISO(data, { + locale: abp.localization.currentCulture.name + }).toLocaleString(); + } + }, + { + title: l('Price'), + data: "price" + }, + { + title: l('CreationTime'), + data: "creationTime", + render: function (data) { + return luxon + .DateTime + .fromISO(data, { + locale: abp.localization.currentCulture.name + }).toLocaleString(luxon.DateTime.DATETIME_SHORT); + } + } + ] + }) + ); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + editModal.onResult(function () { + dataTable.ajax.reload(); + }); + + $('#NewBookButton').click(function (e) { + e.preventDefault(); + createModal.open(); + }); +}); +``` + +* We've used the [Datatables](https://datatables.net/) to list our books. + +* Also defined **create** and **update** modals by using [ABP Modal Manager](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Modals#modalmanager-reference), but we didn't create them yet, so let's create the modals. + +First, create a **CreateModal** razor page and update the **CreateModal.cshtml** and **CreateModal.cshtml.cs** files as below: + +**CreateModal.cshtml** + +```html +@page +@using Acme.BookStore.Web.Pages.Books +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model CreateModalModel +@{ + Layout = null; +} + + + + + + + + + +``` + +* We've used `abp-dynamic-form` tag-helper and passed it a `Book` model, this tag helper will simply create form contents (inputs, select boxes etc.) on behalf of us. + +* **CreateModal.cshtml.cs** + +```csharp +using System.Threading.Tasks; +using Acme.BookStore.Books; +using Microsoft.AspNetCore.Mvc; + +namespace Acme.BookStore.Web.Pages.Books; + +public class CreateModalModel : BookStorePageModel +{ + [BindProperty] + public CreateBookDto Book { get; set; } + + private readonly IBookAppService _bookAppService; + + public CreateModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public void OnGet() + { + Book = new CreateBookDto(); + } + + public async Task OnPostAsync() + { + await _bookAppService.CreateAsync(Book); + return NoContent(); + } +} +``` + +* In this file, we simply define **CreateBookDto** as a bind property and we'll use this class's properties in the form. Thanks to the `abp-dynamic-form` tag-helper we don't need to define all of these form elements one by one, it will generate on behalf of us. + +We can create an **EditModal** razor page and update the **EditModal.cshtml** and **EditModal.cshtml.cs** files as below: + +**EditModal.cshtml** + +```html +@page +@using Acme.BookStore.Web.Pages.Books +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model EditModalModel +@{ + Layout = null; +} +
+ + + + + + + + + + + + +
+``` + +* Here, we didn't use the `abp-dynamic-form` tag-helper and added all the necessary form elements to our form one by one. + +* As you may have noticed, we've set the input type as **hidden** for the **ConcurrencyStamp** input, because the end-user should not see this value. + +> Instead of doing it like that, we could create a view model class and use the `[HiddenInput]` data attribute for the **ConcurrencyStamp** property and use the `abp-dynamic-form` tag-helper. But to simplify the article I didn't want to do that, if you want you can create a view model and define the necessary data attributes for properties. + +**EditModal.cshtml.cs** + +```csharp +public class EditModalModel : BookStorePageModel +{ + [HiddenInput] + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty] + public UpdateBookDto Book { get; set; } + + private readonly IBookAppService _bookAppService; + + public EditModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnGetAsync() + { + var bookDto = await _bookAppService.GetAsync(Id); + Book = ObjectMapper.Map(bookDto); + } + + public async Task OnPostAsync() + { + await _bookAppService.UpdateAsync(Id, Book); + return NoContent(); + } +} +``` + +Lastly, we can define the necessary mapping configurations and run the application to see the result. + +Open the `BookStoreWebAutoMapperProfile.cs` class and update the content as below: + +```csharp +public class BookStoreWebAutoMapperProfile : Profile +{ + public BookStoreWebAutoMapperProfile() + { + CreateMap(); + } +} +``` + +Then we can run the application, navigate to the **/Books** endpoint and see the result. + +![](concurrency-mismatch.gif) + +* In the image above, we can see that multiple users open the edit model to change a record and try to update the relevant record independently of each other. + +* After the first user updated the record, the second user tries to update the same record without getting the last state of the record. And therefore `AbpDbConcurrencyException` is thrown because **ConcurrencyStamp** values are different from each other. + +* The second user should close and re-open the model to get the last state of the record and then they can make changes to the current record. diff --git a/docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/concurrency-mismatch.gif b/docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/concurrency-mismatch.gif new file mode 100644 index 0000000000..922bb5b7ca Binary files /dev/null and b/docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/concurrency-mismatch.gif differ diff --git a/docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/optimistic-concurrency.png b/docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/optimistic-concurrency.png new file mode 100644 index 0000000000..85253594c8 Binary files /dev/null and b/docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/optimistic-concurrency.png differ diff --git a/docs/en/Concurrency-Check.md b/docs/en/Concurrency-Check.md index e22afe82b8..bf31d3db10 100644 --- a/docs/en/Concurrency-Check.md +++ b/docs/en/Concurrency-Check.md @@ -1,3 +1,147 @@ ## Concurrency Check -TODO \ No newline at end of file +### Introduction + +Concurrency Check (also known as **Concurrency Control**) refers to specific mechanisms used to ensure data consistency in the presence of concurrent changes (multiple processes, users access or change the same data in a database at the same time). + +There are two commonly used concurrency control mechanisms/approaches: +* **Optimistic Concurrency Control**: Optimistic Concurrency Control allows multiple users to attempt to **update** the same record without informing the users that others are also attempting to **update** it. + + * If a user successfully updates the record, the other users need to get the latest changes for the current record to be able to make changes. + * ABP's concurrency check system uses the **Optimistic Concurrency Control**. + +* **Pessimistic Concurrency Control**: Pessimistic Concurrency Control prevents simultaneous updates to records and uses a locking mechanism. For more information please see [here](https://www.martinfowler.com/eaaCatalog/pessimisticOfflineLock.html). + +### Usage + +#### `IHasConcurrencyStamp` Interface + +To enable **concurrency control** to your entity class, you should implement the `IHasConcurrencyStamp` interface, directly or indirectly. + +```csharp +public interface IHasConcurrencyStamp +{ + public string ConcurrencyStamp { get; set; } +} +``` + +* It is the base interface for **concurrency control** and only has a simple property named `ConcurrencyStamp`. +* While a new record is **creating**, if the entity implements the `IHasConcurrencyStamp` interface, ABP Framework automatically sets a unique value to the **ConcurrencyStamp** property. +* While a record is **updating**, ABP Framework compares the **ConcurrencyStamp** property of the entity with the provided **ConcurrencyStamp** value by the user and if the values match, it automatically updates the **ConcurrencyStamp** property with the new unique value. If there is a mismatch, `AbpDbConcurrencyException` is thrown. + +**Example: Applying Concurrency Control for the Book Entity** + +Implement the `IHasConcurrencyStamp` interface for your entity: + +```csharp +public class Book : Entity, IHasConcurrencyStamp +{ + public string ConcurrencyStamp { get; set; } + + //... +} +``` + +Also, implement your output and update the DTO classes from the `IHasConcurrencyStamp` interface: + +```csharp +public class BookDto : EntityDto, IHasConcurrencyStamp +{ + //... + + public string ConcurrencyStamp { get; set; } +} + +public class UpdateBookDto : IHasConcurrencyStamp +{ + //... + + public string ConcurrencyStamp { get; set; } +} +``` + +Set the **ConcurrencyStamp** input value to the entity in the **UpdateAsync** method of your application service as below: + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + //... + + public virtual async Task UpdateAsync(Guid id, UpdateBookDto input) + { + var book = await BookRepository.GetAsync(id); + + book.ConcurrencyStamp = input.ConcurrencyStamp; + + //set other input values to the entity ... + + await BookRepository.UpdateAsync(book); + } +} +``` + +* After that, when multiple users try to update the same record at the same time, the concurrency stamp mismatch occurs and `AbpDbConcurrencyException` is thrown. + +#### Base Classes + +[Aggregate Root](./Entities.md#aggregateroot-class) entity classes already implement the `IHasConcurrencyStamp` interface. So, if you are deriving from one of these base classes, you don't need to manually implement the `IHasConcurrencyStamp` interface: + +- `AggregateRoot`, `AggregateRoot` +- `CreationAuditedAggregateRoot`, `CreationAuditedAggregateRoot` +- `AuditedAggregateRoot`, `AuditedAggregateRoot` +- `FullAuditedAggregateRoot`, `FullAuditedAggregateRoot` + +**Example: Applying Concurrency Control for the Book Entity** + +You can inherit your entity from one of [the base classes](#base-classes): + +```csharp +public class Book : FullAuditedAggregateRoot +{ + //... +} +``` + +Then, you can implement your output and update the DTO classes from the `IHasConcurrencyStamp` interface: + +```csharp +public class BookDto : EntityDto, IHasConcurrencyStamp +{ + //... + + public string ConcurrencyStamp { get; set; } +} + +public class UpdateBookDto : IHasConcurrencyStamp +{ + //... + + public string ConcurrencyStamp { get; set; } +} +``` + +Set the **ConcurrencyStamp** input value to the entity in the **UpdateAsync** method of your application service as below: + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + //... + + public virtual async Task UpdateAsync(Guid id, UpdateBookDto input) + { + var book = await BookRepository.GetAsync(id); + + book.ConcurrencyStamp = input.ConcurrencyStamp; + + //set other input values to the entity ... + + await BookRepository.UpdateAsync(book); + } +} +``` + +After that, when multiple users try to update the same record at the same time, the concurrency stamp mismatch occurs and `AbpDbConcurrencyException` is thrown. You can either handle the exception manually or let the ABP Framework handle it for you. + +ABP Framework shows a user-friendly error message as in the image below, if you don't handle the exception manually. + +![Optimistic Concurrency](./images/optimistic-concurrency.png) diff --git a/docs/en/Distributed-Locking.md b/docs/en/Distributed-Locking.md index bea10645ca..d57fd45c83 100644 --- a/docs/en/Distributed-Locking.md +++ b/docs/en/Distributed-Locking.md @@ -27,21 +27,25 @@ using Medallion.Threading.Redis; namespace AbpDemo { - public class MyModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - var configuration = context.Services.GetConfiguration(); - - context.Services.AddSingleton(sp => - { - var connection = ConnectionMultiplexer + [DependsOn( + typeof(AbpDistributedLockingModule) + //If you have the other dependencies, you should do here + )] + public class MyModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + context.Services.AddSingleton(sp => + { + var connection = ConnectionMultiplexer .Connect(configuration["Redis:Configuration"]); - return new + return new RedisDistributedSynchronizationProvider(connection.GetDatabase()); - }); - } - } + }); + } + } } ```` diff --git a/docs/en/Getting-Started-Create-Solution.md b/docs/en/Getting-Started-Create-Solution.md index 8c62246010..d111637365 100644 --- a/docs/en/Getting-Started-Create-Solution.md +++ b/docs/en/Getting-Started-Create-Solution.md @@ -41,19 +41,19 @@ abp new Acme.BookStore{{if UI == "NG"}} -u angular{{else if UI == "Blazor"}} -u > [ABP CLI document](./CLI.md) covers all of the available commands and options. -## Mobile Development +### Mobile Development If you want to include a [React Native](https://reactnative.dev/) project in your solution, add `-m react-native` (or `--mobile react-native`) argument to project creation command. This is a basic React Native startup template to develop mobile applications integrated to your ABP based backends. See the [Getting Started with the React Native](Getting-Started-React-Native.md) document to learn how to configure and run the React Native application. -### The Solution Structure +## The Solution Structure The solution has a layered structure (based on the [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects. See the [application template document](Startup-Templates/Application.md) to understand the solution structure in details. {{ if DB == "Mongo" }} -#### MongoDB Transactions +## MongoDB Transactions The [startup template](Startup-templates/Index.md) **disables** transactions in the `.MongoDB` project by default. If your MongoDB server supports transactions, you can enable it in the *YourProjectMongoDbModule* class's `ConfigureServices` method: diff --git a/docs/en/Modules/Docs.md b/docs/en/Modules/Docs.md index 9653b12618..4777275cd6 100644 --- a/docs/en/Modules/Docs.md +++ b/docs/en/Modules/Docs.md @@ -16,7 +16,7 @@ Docs module is an application module and does not offer any hosting solution. Yo When you use GitHub to store your docs, Docs Module supports versioning. If you have multiple versions for your docs, there will be a combo-box on the UI to switch between versions. If you choose file system to store your docs, it does not support multiple versions. -[The documents](docs.abp.io) for ABP framework is also using this module. +[The documents](https://docs.abp.io/) for ABP framework is also using this module. > Docs module follows the [module architecture best practices](../Best-Practices/Module-Architecture.md) guide. diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index 6d636d5ecb..50b90b516d 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/docs/en/Tutorials/Part-2.md @@ -512,7 +512,7 @@ Open the `/src/app/book/book.component.html` and replace the content as shown be {%{{{ '::Menu:Books' | abpLocalization }}}%} -
+
diff --git a/docs/en/Tutorials/Part-3.md b/docs/en/Tutorials/Part-3.md index 182ffa4005..24f922cb18 100644 --- a/docs/en/Tutorials/Part-3.md +++ b/docs/en/Tutorials/Part-3.md @@ -145,7 +145,7 @@ Open the `Pages/Books/Index.cshtml` and set the content of `abp-card-header` tag @L["Books"] - + @L["Books"] - +
{%{{{ '::Menu:Books' | abpLocalization }}}%}
-
+
-
+
-
-
+
+
diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-II.md b/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-II.md index 763157e049..5ee4531131 100644 --- a/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-II.md +++ b/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-II.md @@ -113,7 +113,7 @@ Abra `Pages/Books/Index.cshtml`e altere a `abp-card-header`tag, como mostrado ab

@L["Books"]

- + `方法提供一个可选参数, `considerOuw`, 默认为`false`. 如果将其设置为`true`, 则你对缓存所做的更改不会应用于真正的缓存存储, 而是与当前的[工作单元](Unit-Of-Work.md)关联. 你将获得在同一工作单元中设置的缓存值, 但**仅当前工作单元成功时**更改才会生效. +`IDistributedCache<..>`方法提供一个可选参数, `considerUow`, 默认为`false`. 如果将其设置为`true`, 则你对缓存所做的更改不会应用于真正的缓存存储, 而是与当前的[工作单元](Unit-Of-Work.md)关联. 你将获得在同一工作单元中设置的缓存值, 但**仅当前工作单元成功时**更改才会生效. ### IDistributedCacheSerializer diff --git a/docs/zh-Hans/Domain-Services.md b/docs/zh-Hans/Domain-Services.md index 9c9bb7dfd2..a617aea645 100644 --- a/docs/zh-Hans/Domain-Services.md +++ b/docs/zh-Hans/Domain-Services.md @@ -1,3 +1,128 @@ -# ABP Documentation +# 领域服务 -待添加 +## 介绍 + +在 [领域驱动设计](Domain-Driven-Design.md) (DDD) 解决方案中,核心业务逻辑通常在聚合 ([实体](Entities.md)) 和领域服务中实现. 在以下情况下特别需要创建领域服务 + +* 你实现了依赖于某些服务(如存储库或其他外部服务)的核心域逻辑. +* 你需要实现的逻辑与多个聚合/实体相关,因此它不适合任何聚合. + +## ABP 领域服务基础设施 + +领域服务是简单的无状态类. 虽然你不必从任何服务或接口派生,但 ABP 框架提供了一些有用的基类和约定. + +### DomainService 和 IDomainService + +从 `DomainService` 基类派生领域服务或直接实现 `IDomainService` 接口. + +**示例: 创建从 `DomainService` 基类派生的领域服务.** + +````csharp +using Volo.Abp.Domain.Services; +namespace MyProject.Issues +{ + public class IssueManager : DomainService + { + + } +} +```` + +当你这样做时: + +* ABP 框架自动将类注册为瞬态生命周期到依赖注入系统. +* 你可以直接使用一些常用服务作为基础属性,而无需手动注入 (例如 [ILogger](Logging.md) and [IGuidGenerator](Guid-Generation.md)). + +> 建议使用 `Manager` 或 `Service` 后缀命名领域服务. 我们通常使用如上面示例中的 `Manager` 后缀. +**示例: 实现将问题分配给用户的领域逻辑** + +````csharp +public class IssueManager : DomainService +{ + private readonly IRepository _issueRepository; + public IssueManager(IRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task AssignAsync(Issue issue, AppUser user) + { + var currentIssueCount = await _issueRepository + .CountAsync(i => i.AssignedUserId == user.Id); + + //Implementing a core business validation + if (currentIssueCount >= 3) + { + throw new IssueAssignmentException(user.UserName); + } + issue.AssignedUserId = user.Id; + } +} +```` + +问题是定义如下所示的 [聚合根](Entities.md): + +````csharp +public class Issue : AggregateRoot +{ + public Guid? AssignedUserId { get; internal set; } + + //... +} +```` + +* 使用 `internal` 的 set 确保外层调用者不能直接在调用 set ,并强制始终使用 `IssueManager` 为 `User` 分配 `Issue`. + +### 使用领域服务 + +领域服务通常用于 [应用程序服务](Application-Services.md). + +**示例: 使用 `IssueManager` 将问题分配给用户** + +````csharp +using System; +using System.Threading.Tasks; +using MyProject.Users; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; +namespace MyProject.Issues +{ + public class IssueAppService : ApplicationService, IIssueAppService + { + private readonly IssueManager _issueManager; + private readonly IRepository _userRepository; + private readonly IRepository _issueRepository; + public IssueAppService( + IssueManager issueManager, + IRepository userRepository, + IRepository issueRepository) + { + _issueManager = issueManager; + _userRepository = userRepository; + _issueRepository = issueRepository; + } + public async Task AssignAsync(Guid id, Guid userId) + { + var issue = await _issueRepository.GetAsync(id); + var user = await _userRepository.GetAsync(userId); + await _issueManager.AssignAsync(issue, user); + await _issueRepository.UpdateAsync(issue); + } + } +} +```` + +由于 `IssueAppService` 在应用层, 它不能直接将问题分配给用户.因此,它使用 `IssueManager`. + +## 应用程序服务与领域服务 + +虽然应用服务和领域服务都实现了业务规则,但存在根本的逻辑和形式差异; +虽然 [应用服务](Application-Services.md) 和领域服务都实现了业务规则,但存在根本的逻辑和形式差异: + +* 应用程序服务实现应用程序的 **用例** (典型 Web 应用程序中的用户交互), 而领域服务实现 **核心的、用例独立的领域逻辑**. +* 应用程序服务获取/返回 [数据传输对象](Data-Transfer-Objects.md), 领域服务方法通常获取和返回 **领域对象** ([实体](Entities.md), [值对象](Value-Objects.md)). +* 领域服务通常由应用程序服务或其他领域服务使用,而应用程序服务由表示层或客户端应用程序使用. + +## 生命周期 + +领域服务的生命周期是 [瞬态](https://docs.abp.io/en/abp/latest/Dependency-Injection) 的,它们会自动注册到依赖注入服务. diff --git a/docs/zh-Hans/Tutorials/Part-2.md b/docs/zh-Hans/Tutorials/Part-2.md index 6a0d549abe..346927acbe 100644 --- a/docs/zh-Hans/Tutorials/Part-2.md +++ b/docs/zh-Hans/Tutorials/Part-2.md @@ -514,7 +514,7 @@ export class BookComponent implements OnInit { {%{{{ '::Menu:Books' | abpLocalization }}}%}
-
+
diff --git a/docs/zh-Hans/Tutorials/Part-3.md b/docs/zh-Hans/Tutorials/Part-3.md index 9cd464700a..b8d56fb26a 100644 --- a/docs/zh-Hans/Tutorials/Part-3.md +++ b/docs/zh-Hans/Tutorials/Part-3.md @@ -145,7 +145,7 @@ namespace Acme.BookStore.Web.Pages.Books @L["Books"] - + @L["Books"] - +
{%{{{ '::Menu:Books' | abpLocalization }}}%}
-
-
- +
+
-
+