diff --git a/.gitignore b/.gitignore
index 73d74ec7c6..6f7330998e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -291,3 +291,4 @@ samples/MicroserviceDemo/gateways/PublicWebSiteGateway.Host/Logs/logs.txt
samples/MicroserviceDemo/microservices/BloggingService.Host/Logs/logs.txt
samples/MicroserviceDemo/applications/ConsoleClientDemo/Logs/logs.txt
modules/docs/app/Volo.DocsTestApp/Logs/logs.txt
+framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Logs/logs.txt
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
index 9c00e584ab..5ebdf0b2eb 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
@@ -53,6 +53,7 @@
"CreateANugetPackage": "Create a Nuget package",
"AddNew": "Add new",
"PackageAlreadyExist{0}": "\"{0}\" package is already added.",
+ "ModuleAlreadyExist{0}": "\"{0}\" module is already added.",
"ClearCache": "Clear cache",
"SuccessfullyCleared": "Successfully cleared",
"Menu:NpmPackages": "NPM Packages",
@@ -83,6 +84,7 @@
"This{0}AlreadyExistInThisOrganization": "This {0} already exist in this organization",
"AreYouSureYouWantToDeleteAllComputers": "Are you sure you want to delete all computers?",
"DeleteAll": "Delete all",
- "DoYouWantToCreateNewUser": "Do you want to create new user?"
+ "DoYouWantToCreateNewUser": "Do you want to create new user?",
+ "MasterModules": "Master Modules"
}
}
\ No newline at end of file
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json
index 7987b9ab32..3df70437b4 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json
@@ -5,8 +5,9 @@
"OrganizationList": "Organization list",
"Volo.AbpIo.Commercial:010003": "You are not owner of this organization!",
"OrganizationNotFoundMessage": "No organization found!",
- "DeveloperCount": "Developer count",
- "QuestionCount": "Question count",
+ "DeveloperCount": "Allocated / total developers",
+ "QuestionCount": "Remaining / total questions",
+ "Unlimited": "Unlimited",
"Owners": "Owners",
"AddMember": "Add member",
"AddOwner": "Add owner",
@@ -30,5 +31,5 @@
"SuccessfullyAddedToNewsletter": "Thanks you for subscribing to our newsletter!",
"ManageProfile": "Manage your profile",
"EmailNotValid": "Please enter a valid email address."
- }
+ }
}
\ No newline at end of file
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
index be193b7396..2502dce76a 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
@@ -154,6 +154,8 @@
"SeeTheDocumentForMoreInformation": "See the {0} document for more information",
"IndexPageHeroSection": "open sourceWeb Application Framework for asp.net core",
"UiFramework": "UI Framework",
- "EmailAddress": "Email address"
+ "EmailAddress": "Email address",
+ "Mobile": "Mobile",
+ "ReactNative": "React Native"
}
}
\ No newline at end of file
diff --git a/common.props b/common.props
index 6f12895d7c..92d87d3d76 100644
--- a/common.props
+++ b/common.props
@@ -1,7 +1,7 @@
- latest
- 2.2.0
+ latest
+ 2.3.0$(NoWarn);CS1591https://abp.io/assets/abp_nupkg.pnghttps://abp.io
@@ -9,9 +9,7 @@
githttps://github.com/abpframework/abp/
-
-
\ No newline at end of file
diff --git a/docs/en/Background-Jobs-Quartz.md b/docs/en/Background-Jobs-Quartz.md
new file mode 100644
index 0000000000..8039192708
--- /dev/null
+++ b/docs/en/Background-Jobs-Quartz.md
@@ -0,0 +1,73 @@
+# Quartz Background Job Manager
+
+[Quartz](https://www.quartz-scheduler.net/) is an advanced background job manager. You can integrate Quartz with the ABP Framework to use it instead of the [default background job manager](Background-Jobs.md). In this way, you can use the same background job API for Quartz and your code will be independent of Quartz. If you like, you can directly use Quartz's API, too.
+
+> See the [background jobs document](Background-Jobs.md) to learn how to use the background job system. This document only shows how to install and configure the Quartz integration.
+
+## Installation
+
+It is suggested to use the [ABP CLI](CLI.md) to install this package.
+
+### Using the ABP CLI
+
+Open a command line window in the folder of the project (.csproj file) and type the following command:
+
+````bash
+abp add-package Volo.Abp.BackgroundJobs.Quartz
+````
+
+### Manual Installation
+
+If you want to manually install;
+
+1. Add the [Volo.Abp.BackgroundJobs.Quartz](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.Quartz) NuGet package to your project:
+
+ ````
+ Install-Package Volo.Abp.BackgroundJobs.Quartz
+ ````
+
+2. Add the `AbpBackgroundJobsQuartzModule` to the dependency list of your module:
+
+````csharp
+[DependsOn(
+ //...other dependencies
+ typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency
+ )]
+public class YourModule : AbpModule
+{
+}
+````
+
+## Configuration
+
+Quartz is a very configurable library,and the ABP framework provides `AbpQuartzPreOptions` for this. You can use the `PreConfigure` method in your module class to pre-configure this option. ABP will use it when initializing the Quartz module. For example:
+
+````csharp
+[DependsOn(
+ //...other dependencies
+ typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency
+ )]
+public class YourModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ var configuration = context.Services.GetConfiguration();
+
+ PreConfigure(options =>
+ {
+ options.Properties = new NameValueCollection
+ {
+ ["quartz.jobStore.dataSource"] = "BackgroundJobsDemoApp",
+ ["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
+ ["quartz.jobStore.tablePrefix"] = "QRTZ_",
+ ["quartz.serializer.type"] = "json",
+ ["quartz.dataSource.BackgroundJobsDemoApp.connectionString"] = configuration.GetConnectionString("Quartz"),
+ ["quartz.dataSource.BackgroundJobsDemoApp.provider"] = "SqlServer",
+ ["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz",
+ };
+ });
+ }
+}
+````
+
+Quartz stores job and scheduling information **in memory by default**. In the example, we use the pre-configuration of [options pattern](Options.md) to change it to the database. For more configuration of Quartz, please refer to the Quartz's [documentation](https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/index.html).
\ No newline at end of file
diff --git a/docs/en/Background-Jobs.md b/docs/en/Background-Jobs.md
index 70f06bbadb..7162359662 100644
--- a/docs/en/Background-Jobs.md
+++ b/docs/en/Background-Jobs.md
@@ -11,7 +11,7 @@ Background jobs are **persistent** that means they will be **re-tried** and **ex
## Abstraction Package
-ABP provides an **abstraction** module and **several implementations** for background jobs. It has a built-in/default implementation as well as Hangfire and RabbitMQ integrations.
+ABP provides an **abstraction** module and **several implementations** for background jobs. It has a built-in/default implementation as well as Hangfire, RabbitMQ and Quartz integrations.
`Volo.Abp.BackgroundJobs.Abstractions` nuget package provides needed services to create background jobs and queue background job items. If your module only depend on this package, it can be independent from the actual implementation/integration.
diff --git a/docs/en/Background-Worker.md b/docs/en/Background-Worker.md
new file mode 100644
index 0000000000..13dd7246fc
--- /dev/null
+++ b/docs/en/Background-Worker.md
@@ -0,0 +1,3 @@
+# Background Workers
+
+TODO
\ No newline at end of file
diff --git a/docs/en/Background-Workers-Quartz.md b/docs/en/Background-Workers-Quartz.md
new file mode 100644
index 0000000000..523704cb64
--- /dev/null
+++ b/docs/en/Background-Workers-Quartz.md
@@ -0,0 +1,68 @@
+# Quartz Background Worker Manager
+
+[Quartz](https://www.quartz-scheduler.net/) is an advanced background worker manager. You can integrate Quartz with the ABP Framework to use it instead of the [default background worker manager](Background-Worker.md). ABP simply integrates quartz.
+
+## Installation
+
+It is suggested to use the [ABP CLI](CLI.md) to install this package.
+
+### Using the ABP CLI
+
+Open a command line window in the folder of the project (.csproj file) and type the following command:
+
+````bash
+abp add-package Volo.Abp.BackgroundWorkers.Quartz
+````
+
+### Manual Installation
+
+If you want to manually install;
+
+1. Add the [Volo.Abp.BackgroundWorkers.Quartz](https://www.nuget.org/packages/Volo.Abp.BackgroundWorkers.Quartz) NuGet package to your project:
+
+ ````
+ Install-Package Volo.Abp.BackgroundWorkers.Quartz
+ ````
+
+2. Add the `AbpBackgroundWorkersQuartzModule` to the dependency list of your module:
+
+````csharp
+[DependsOn(
+ //...other dependencies
+ typeof(AbpBackgroundWorkersQuartzModule) //Add the new module dependency
+ )]
+public class YourModule : AbpModule
+{
+}
+````
+
+### Configuration
+
+See [Configuration](Background-Jobs-Quartz.md#Configuration).
+
+### Create a Background Worker
+
+A background work is a class that derives from the `QuartzBackgroundWorkerBase` base class. for example. A simple worker class is shown below:
+
+```` csharp
+public class MyLogWorker : QuartzBackgroundWorkerBase
+{
+ public MyLogWorker()
+ {
+ JobDetail = JobBuilder.Create().Build();
+ Trigger = TriggerBuilder.Create().StartNow().Build();
+ }
+
+ public override Task Execute(IJobExecutionContext context)
+ {
+ Logger.LogInformation("Executed MyLogWorker..!");
+ return Task.CompletedTask;
+ }
+}
+````
+
+We simply implemented the Execute method to write a log. The background worker is a **singleton by default**. If you want, you can also implement a [dependency interface](Dependency-Injection.md#DependencyInterfaces) to register it as another life cycle.
+
+### More
+
+Please see Quartz's [documentation](https://www.quartz-scheduler.net/documentation/index.html) for more information.
\ No newline at end of file
diff --git a/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md b/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md
index eec70c5566..4077633015 100644
--- a/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md
+++ b/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md
@@ -12,11 +12,11 @@ Finally, ABP has a **SPA UI** option with the latest [Angular](https://angular.i
* Created Angular UI packages for the modules like account, identity and tenant-management.
* Created a minimal startup template that authenticates using IdentityServer and uses the ASP.NET Core backend. This template uses the packages mentioned above.
* Worked on the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) and the [download page](https://abp.io/get-started) to be able to generate projects with the new UI option.
-* Created a [tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Angular/Part-I) to jump start with the new UI option.
+* Created a [tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG) to jump start with the new UI option.
We've created the template, document and infrastructure based on the latest Angular tools and trends:
-* Uses [NgBootstrap](https://ng-bootstrap.github.io/) and [PrimeNG](https://www.primefaces.org/primeng/) as the UI component libraries. You can use your favorite library, no problem, but pre-built modules work with these libraries.
+* Uses [NgBootstrap](https://ng-bootstrap.github.io/) as the UI component library. You can use your favorite library, but pre-built modules work with these libraries.
* Uses [NGXS](https://ngxs.gitbook.io/ngxs/) as the state management library.
Angular was the first SPA UI option, but it is not the last. After v1.0 release, we will start to work on a second UI option. Not decided yet, but candidates are Blazor, React and Vue.js. Waiting your feedback. You can thumb up using the following issues:
diff --git a/docs/en/CLI.md b/docs/en/CLI.md
index a1d3d51138..a1c53e0407 100644
--- a/docs/en/CLI.md
+++ b/docs/en/CLI.md
@@ -51,10 +51,11 @@ abp new Acme.BookStore
* `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers:
* `ef`: Entity Framework Core.
* `mongodb`: MongoDB.
- * `module`: [Module template](Startup-Templates/Module.md). Additional options:
- * `--no-ui`: Specifies to not include the UI. This makes possible to create service-only modules (a.k.a. microservices - without UI).
+ * `module`: [Module template](Startup-Templates/Module.md). Additional options:
+ * `--no-ui`: Specifies to not include the UI. This makes possible to create service-only modules (a.k.a. microservices - without UI).
* `--output-folder` or `-o`: Specifies the output folder. Default value is the current directory.
* `--version` or `-v`: Specifies the ABP & template version. It can be a [release tag](https://github.com/abpframework/abp/releases) or a [branch name](https://github.com/abpframework/abp/branches). Uses the latest release if not specified. Most of the times, you will want to use the latest version.
+* `--template-source` or `-ts`: Specifies a custom template source to use to build the project. Local and network sources can be used(Like `D\localTemplate` or `https://.zip`).
### add-package
@@ -128,9 +129,35 @@ abp update [options]
* `--npm`: Only updates NPM packages.
* `--nuget`: Only updates NuGet packages.
+### switch-to-preview
+
+You can use this command to switch your project to latest preview version of the ABP framework packages.
+
+Usage:
+
+````bash
+abp switch-to-preview [options]
+````
+#### Options
+
+`--solution-path` or `-sp`: Specifies the solution (.sln) file path. If not specified, CLI tries to find a .sln file in the current directory.
+
+### switch-to-stable
+
+If you're using the ABP Framework preview packages, you can switch back to stable version using this command.
+
+Usage:
+
+````bash
+abp switch-to-stable [options]
+````
+#### Options
+
+`--solution-path` or `-sp`: Specifies the solution (.sln) file path. If not specified, CLI tries to find a .sln file in the current directory.
+
### login
-Some features of the CLI requires to be logged in to abp.io platform. To login with your username write
+Some features of the CLI requires to be logged in to abp.io platform. To login with your username write:
```bash
abp login
@@ -161,5 +188,4 @@ Examples:
````bash
abp help # Shows a general help.
abp help new # Shows help about the "new" command.
-````
-
+````
\ No newline at end of file
diff --git a/docs/en/Entity-Framework-Core-Migrations.md b/docs/en/Entity-Framework-Core-Migrations.md
new file mode 100644
index 0000000000..23b17293f1
--- /dev/null
+++ b/docs/en/Entity-Framework-Core-Migrations.md
@@ -0,0 +1,912 @@
+# EF Core Database Migrations
+
+This document begins by **introducing the default structure** provided by [the application startup template](Startup-Templates/Application.md) and **discusses various scenarios** you may want to implement for your own application.
+
+> This document is for who want to fully understand and customize the database structure comes with [the application startup template](Startup-Templates/Application.md). If you simply want to create entities and manage your code first migrations, just follow [the startup tutorials](Tutorials/Index.md).
+
+### Source Code
+
+You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp/tree/dev/samples/EfCoreMigrationDemo). However, you need to read and understand this document in order to understand the example project's source code.
+
+## About the EF Core Code First Migrations
+
+Entity Framework Core provides an easy to use and powerful [database migration system](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). ABP Framework [startup templates](Startup-Templates/Index.md) take the advantage of this system to allow you to develop your application in a standard way.
+
+However, EF Core migration system is **not so good in a modular environment** where each module maintains its **own database schema** while two or more modules may **share a single database** in practical.
+
+Since ABP Framework cares about modularity in all aspects, it provides a **solution** to this problem. It is important to understand this solution if you need to **customize your database structure**.
+
+> See [EF Core's own documentation](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to fully learn the EF Core Code First Migrations and why you need to such a system.
+
+## The Default Solution & Database Configuration
+
+When you [create a new web application](https://abp.io/get-started) (with EF Core, which is the default database provider), your solution structure will be similar to the picture below:
+
+
+
+Actual solution structure may be a bit different based on your preferences, but the database part will be same.
+
+> This document will use the `Acme.BookStore` example project name to refer the projects and classes. You need to find the corresponding class/project in your solution.
+
+### The Database Structure
+
+The startup template has some [application modules](Modules/Index.md) pre-installed. Each layer of the solution has corresponding module **package references**. So, the `.EntityFrameworkCore` project has the NuGet references for the `.EntityFrameworkCore` packages of the used modules:
+
+
+
+In this way, you collect all the **EF Core dependencies** under the `.EntityFrameworkCore` project.
+
+> In addition to the module references, it references to the `Volo.Abp.EntityFrameworkCore.SqlServer` package since the startup template is pre-configured for the **SQL Server**. See the documentation if you want to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md).
+
+While every module has its own `DbContext` class by design and can use its **own physical database**, the solution is configured to use a **single shared database** as shown in the figure below:
+
+
+
+This is **the simplest configuration** and suitable for most of the applications. `appsettings.json` file has a **single connection string**, named `Default`:
+
+````json
+"ConnectionStrings": {
+ "Default": "..."
+}
+````
+
+So, you have a **single database schema** which contains all the tables of the modules **sharing** this database.
+
+ABP Framework's [connection string](Connection-Strings.md) system allows you to easily **set a different connection string** for a desired module:
+
+````json
+"ConnectionStrings": {
+ "Default": "...",
+ "AbpAuditLogging": "..."
+}
+````
+
+The example configuration about tells to the ABP Framework to use the second connection string for the [Audit Logging module](Modules/Audit-Logging.md).
+
+**However, this is just the beginning**. You also need to create the second database, create audit log tables inside it and maintain the database tables using the code first migrations approach. One of the main purposes of this document is to guide you on such **database separation** scenarios.
+
+#### Module Tables
+
+Every module uses its **own databases tables**. For example, the [Identity Module](Modules/Identity.md) has some tables to manage the users and roles in the system.
+
+##### Table Prefixes
+
+Since it is allowed to share a single database by all modules (it is the default configuration), a module typically uses a **table name prefix** to group its own tables.
+
+The fundamental modules, like [Identity](Modules/Identity.md), [Tenant Management](Modules/Tenant-Management.md) and [Audit Logs](Modules/Audit-Logging.md), use the `Abp` prefix, while some other modules use their own prefixes. [Identity Server](Modules/IdentityServer.md) module uses the `IdentityServer` prefix for example.
+
+If you want, you can **change the database table name prefix** for a module for your application. Example:
+
+````csharp
+Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids";
+````
+
+This code changes the prefix of the [Identity Server](Modules/IdentityServer.md) module. Write this code **at the very beginning** in your application.
+
+> Every module also defines `DbSchema` property (near to `DbTablePrefix`), so you can set it for the databases support the schema usage.
+
+### The Projects
+
+From the database point of view, there are three important projects those will be explained in the next sections.
+
+#### .EntityFrameworkCore Project
+
+This project has the `DbContext` class (`BookStoreDbContext` for this sample) of your application.
+
+**Every module uses its own `DbContext` class** to access to the database. Likewise, your application has its own `DbContext`. You typically use this `DbContext` in your application code (in your custom [repositories](Repositories.md) if you follow the best practices). It is almost an empty `DbContext` since your application don't have any entities at the beginning, except the pre-defined `AppUser` entity:
+
+````csharp
+[ConnectionStringName("Default")]
+public class BookStoreDbContext : AbpDbContext
+{
+ public DbSet Users { get; set; }
+
+ /* Add DbSet properties for your Aggregate Roots / Entities here. */
+
+ public BookStoreDbContext(DbContextOptions options)
+ : base(options)
+ {
+
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+
+ /* Configure the shared tables (with included modules) here */
+
+ builder.Entity(b =>
+ {
+ //Sharing the same table "AbpUsers" with the IdentityUser
+ b.ToTable("AbpUsers");
+
+ //Configure base properties
+ b.ConfigureByConvention();
+ b.ConfigureAbpUser();
+
+ //Moved customization of the "AbpUsers" table to an extension method
+ b.ConfigureCustomUserProperties();
+ });
+
+ /* Configure your own tables/entities inside the ConfigureBookStore method */
+ builder.ConfigureBookStore();
+ }
+}
+````
+
+This simple `DbContext` class still needs some explanations:
+
+* It defines a `[ConnectionStringName]` attribute which tells ABP to always use the `Default` connection string for this `Dbcontext`.
+* It inherits from the `AbpDbContext` instead of the standard `DbContext` class. You can see the [EF Core integration](Entity-Framework-Core.md) document for more. For now, know that the `AbpDbContext` base class implements some conventions of the ABP Framework to automate some common tasks for you.
+* It declares a `DbSet` property for the `AppUser` entity. `AppUser` shares the same table (named `AbpUsers` by default) with the `IdentityUser` entity of the [Identity module](Modules/Identity.md). The startup template provides this entity inside the application since we think that the User entity is generally needs to be customized in your application.
+* The constructor takes a `DbContextOptions` instance.
+* It overrides the `OnModelCreating` method to define the EF Core mappings.
+ * It first calls the the `base.OnModelCreating` method to let the ABP Framework to implement the base mappings for us.
+ * It then configures the mapping for the `AppUser` entity. There is a special case for this entity (it shares a table with the Identity module), which will be explained in the next sections.
+ * It finally calls the `builder.ConfigureBookStore()` extension method to configure other entities of your application.
+
+This design will be explained in more details after introducing the other database related projects.
+
+#### .EntityFrameworkCore.DbMigrations Project
+
+As mentioned in the previous section, every module (and your application) have **their own** separate `DbContext` classes. Each `DbContext` class only defines the entity to table mappings related to its own module and each module (and your application) use the related `DbContext` class **on runtime**.
+
+As you know, EF Core Code First migration system relies on a `DbContext` class **to track and generate** the code first migrations. So, which `DbContext` we should use for the migrations? The answer is *none of them*. There is another `DbContext` defined in the `.EntityFrameworkCore.DbMigrations` project (which is the `BookStoreMigrationsDbContext` for this example solution).
+
+##### The MigrationsDbContext
+
+The `MigrationsDbContext` is only used to create and apply the database migrations. It is **not used on runtime**. It **merges** all the entity to table mappings of all the used modules plus the application's mappings.
+
+In this way, you create and maintain a **single database migration path**. However, there are some difficulties of this approach and the next sections explains how ABP Framework overcomes these difficulties. But first, see the `BookStoreMigrationsDbContext` class as an example:
+
+````csharp
+/* This DbContext is only used for database migrations.
+ * It is not used on runtime. See BookStoreDbContext for the runtime DbContext.
+ * It is a unified model that includes configuration for
+ * all used modules and your application.
+ */
+public class BookStoreMigrationsDbContext : AbpDbContext
+{
+ public BookStoreMigrationsDbContext(
+ DbContextOptions options)
+ : base(options)
+ {
+
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+
+ /* Include modules to your migration db context */
+ builder.ConfigurePermissionManagement();
+ builder.ConfigureSettingManagement();
+ builder.ConfigureBackgroundJobs();
+ builder.ConfigureAuditLogging();
+ builder.ConfigureIdentity();
+ builder.ConfigureIdentityServer();
+ builder.ConfigureFeatureManagement();
+ builder.ConfigureTenantManagement();
+
+ /* Configure customizations for entities from the modules included */
+ builder.Entity(b =>
+ {
+ b.ConfigureCustomUserProperties();
+ });
+
+ /* Configure your own tables/entities inside the ConfigureBookStore method */
+ builder.ConfigureBookStore();
+ }
+}
+````
+
+##### Sharing the Mapping Code
+
+First problem is that: A module uses its own `DbContext` which needs to the database mappings. The `MigrationsDbContext` also needs to the same mapping in order to create the database tables for this module. We definitely **don't want to duplicate** the mapping code.
+
+The solution is to define an **extension method** (on the `ModelBuilder`) that can be called by both `DbContext` classes. So, all modules define such extension methods.
+
+For example, the `builder.ConfigureBackgroundJobs()` method call configures the database tables for the [Background Jobs module](Modules/Background-Jobs.md). The definition of this extension method is something like that:
+
+````csharp
+public static class BackgroundJobsDbContextModelCreatingExtensions
+{
+ public static void ConfigureBackgroundJobs(
+ this ModelBuilder builder,
+ Action optionsAction = null)
+ {
+ var options = new BackgroundJobsModelBuilderConfigurationOptions(
+ BackgroundJobsDbProperties.DbTablePrefix,
+ BackgroundJobsDbProperties.DbSchema
+ );
+
+ optionsAction?.Invoke(options);
+
+ builder.Entity(b =>
+ {
+ b.ToTable(options.TablePrefix + "BackgroundJobs", options.Schema);
+
+ b.ConfigureCreationTime();
+ b.ConfigureExtraProperties();
+
+ b.Property(x => x.JobName)
+ .IsRequired()
+ .HasMaxLength(BackgroundJobRecordConsts.MaxJobNameLength);
+
+ //...
+ });
+ }
+}
+````
+
+This extension method also gets options to change the database table prefix and schema for this module, but it is not important here.
+
+The final application calls the extension methods inside the `MigrationsDbContext` class, so it can decide which modules are included in the database maintained by this `MigrationsDbContext`. If you want to create a second database and move some module tables to the second database, then you need to have a second `MigrationsDbContext` class which only calls the extension methods of the related modules. This topic will be detailed in the next sections.
+
+The same `ConfigureBackgroundJobs` method is also called in the `DbContext` of the Background Jobs module:
+
+````csharp
+[ConnectionStringName(BackgroundJobsDbProperties.ConnectionStringName)]
+public class BackgroundJobsDbContext
+ : AbpDbContext, IBackgroundJobsDbContext
+{
+ public DbSet BackgroundJobs { get; set; }
+
+ public BackgroundJobsDbContext(DbContextOptions options)
+ : base(options)
+ {
+
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+
+ //Reuse the same extension method!
+ builder.ConfigureBackgroundJobs();
+ }
+}
+````
+
+In this way, the mapping configuration of a module can be shared between `DbContext` classes. The code above is inside the related module NuGet package, so you don't care about it.
+
+##### Reusing a Table of a Module
+
+You may want to **reuse a table** of a depended module in your application. In this case, you have two options:
+
+1. You can **directly use the entity** defined by the module.
+2. You can **create a new entity** mapping to the same database table.
+
+###### Use the Entity Defined by a Module
+
+Using an entity defined a module is pretty easy and standard. For example, Identity module defines the `IdentityUser` entity. You can inject the [repository](Repositories.md) for the `IdentityUser` and perform the standard repository operations for this entity. Example:
+
+````csharp
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Domain.Repositories;
+using Volo.Abp.Identity;
+
+namespace Acme.BookStore
+{
+ public class MyService : ITransientDependency
+ {
+ private readonly IRepository _identityUserRepository;
+
+ public MyService(IRepository identityUserRepository)
+ {
+ _identityUserRepository = identityUserRepository;
+ }
+
+ public async Task DoItAsync()
+ {
+ //Get all users
+ var users = await _identityUserRepository.GetListAsync();
+ }
+ }
+}
+````
+
+This example injects the `IRepository` (default repository) which defines the standard repository methods and implements the `IQueryable` interface.
+
+> In addition, Identity module defines the `IIdentityUserRepository` (custom repository) that can also be injected and used by your application. `IIdentityUserRepository` provides additional custom methods for the `IdentityUser` entity while it does not implement the `IQueryable` interface.
+
+###### Create a New Entity
+
+Working with an entity of a module is easy if you want to use the entity as is. However, you may want to define your own entity class and map to the same database table in the following cases;
+
+* You want to **add a new field** to the table and map it to a property in the entity. You can't use the module's entity since it doesn't have the related property.
+* You want to **use a subset of the table fields**. You don't want to access to all properties of the entity and hide the unrelated properties (from a security perspective or just by design).
+* You don't want to directly **depend on** a module entity class.
+
+In any case, the progress is same. Assume that you want to create an entity, named `AppRole`, mapped to the same table of the `IdentityRole` entity of the [Identity module](Modules/Identity.md).
+
+Here, we will show the implementation, then **will discuss the limitations** of this approach.
+
+First, create a new `AppRole` class in your `.Domain` project:
+
+````csharp
+using System;
+using Volo.Abp.Domain.Entities;
+using Volo.Abp.MultiTenancy;
+
+namespace Acme.BookStore.Roles
+{
+ public class AppRole : AggregateRoot, IMultiTenant
+ {
+ // Properties shared with the IdentityRole class
+
+ public Guid? TenantId { get; private set; }
+ public string Name { get; private set; }
+
+ //Additional properties
+
+ public string Title { get; set; }
+
+ private AppRole()
+ {
+
+ }
+ }
+}
+````
+
+* It's inherited from [the `AggregateRoot` class](Entities.md) and implements [the `IMultiTenant` interface](Multi-Tenancy.md) because the `IdentityRole` also does the same.
+* You can add any properties defined by the `IdentityRole` entity. This examples add only the `TenantId` and `Name` properties since we only need them here. You can make the setters private (like in this example) to prevent changing Identity module's properties accidently.
+* You can add custom (additional) properties. This example adds the `Title` property.
+* The **constructor is private**, so it is not allowed to directly create a new `AppRole` entity. Creating a role is a responsibility of the Identity module. You can query roles, set/update your custom properties, but you should not create or delete a role in your code, as a best practice (while there is nothing restricts you).
+
+Now, it is time to define the EF Core mappings. Open the `DbContext` of your application (`BookStoreDbContext` in this sample) and add the following property:
+
+````csharp
+public DbSet Roles { get; set; }
+````
+
+Then configure the mapping inside the `OnModelCreating` method (after calling the `base.OnModelCreating(builder)`):
+
+````csharp
+protected override void OnModelCreating(ModelBuilder builder)
+{
+ base.OnModelCreating(builder);
+
+ /* Configure the shared tables (with included modules) here */
+
+ //CONFIGURE THE AppRole ENTITY
+ builder.Entity(b =>
+ {
+ b.ToTable("AbpRoles");
+
+ b.ConfigureByConvention();
+
+ b.ConfigureCustomRoleProperties();
+ });
+
+ ...
+
+ /* Configure your own tables/entities inside the ConfigureBookStore method */
+
+ builder.ConfigureBookStore();
+}
+````
+
+We added the following lines:
+
+````csharp
+builder.Entity(b =>
+{
+ b.ToTable("AbpRoles");
+
+ b.ConfigureByConvention();
+
+ b.ConfigureCustomRoleProperties();
+});
+````
+
+* It maps to the same `AbpRoles` table shared with the `IdentityRole` entity.
+* `ConfigureByConvention()` configures the standard/base properties (like `TenantId`) and recommended to always call it.
+
+`ConfigureCustomRoleProperties()` has not exists yet. Define it inside the `BookStoreDbContextModelCreatingExtensions` class (near to your `DbContext` in the `.EntityFrameworkCore` project):
+
+````csharp
+public static void ConfigureCustomRoleProperties(this EntityTypeBuilder b)
+ where TRole : class, IEntity
+{
+ b.Property(nameof(AppRole.Title)).HasMaxLength(128);
+}
+````
+
+* This method only defines the **custom properties** of your entity.
+* Unfortunately, we can not utilize the fully **type safety** here (by referencing the `AppRole` entity). The best we can do is to use the `Title` name as type safe. This is because of EF Core migration system can not map two unrelated entity classes to the same database table.
+
+You've configured the custom property for your `DbContext` used by your application on the runtime. We also need to configure the `MigrationsDbContext`.
+
+Open the `MigrationsDbContext` (`BookStoreMigrationsDbContext` for this example) and change as shown below:
+
+````csharp
+protected override void OnModelCreating(ModelBuilder builder)
+{
+ base.OnModelCreating(builder);
+
+ /* Include modules to your migration db context */
+
+ ...
+
+ /* Configure customizations for entities from the modules included */
+
+ //CONFIGURE THE CUSTOM ROLE PROPERTIES
+ builder.Entity(b =>
+ {
+ b.ConfigureCustomRoleProperties();
+ });
+
+ ...
+
+ /* Configure your own tables/entities inside the ConfigureBookStore method */
+
+ builder.ConfigureBookStore();
+}
+````
+
+Only added the following lines:
+
+````csharp
+builder.Entity(b =>
+{
+ b.ConfigureCustomRoleProperties();
+});
+````
+
+In this way, we re-used the extension method that is used to configure custom property mappings for the role. But, this time, did the same customization for the `IdentityRole` entity.
+
+Now, you can add a new EF Core database migration using the standard `Add-Migration` command in the Package Manager Console (remember to select `.EntityFrameworkCore.DbMigrations` as the Default Project in the PMC and make sure that the `.Web` project is still the startup project):
+
+
+
+This command will create a new code first migration class as shown below:
+
+````csharp
+public partial class Added_Title_To_Roles : Migration
+{
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "Title",
+ table: "AbpRoles",
+ maxLength: 128,
+ nullable: true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Title",
+ table: "AbpRoles");
+ }
+}
+````
+
+All done! Just run the `Update-Database` command in the PMC or run the `.DbMigrator` project in your solution to apply changes to database.
+
+Now, you can work with the `AppRole` entity just like any other entity of your application. An example [application service](Application-Services.md) that queries and updates roles:
+
+````csharp
+public class AppRoleAppService : ApplicationService, IAppRoleAppService
+{
+ private readonly IRepository _appRoleRepository;
+
+ public AppRoleAppService(IRepository appRoleRepository)
+ {
+ _appRoleRepository = appRoleRepository;
+ }
+
+ public async Task> GetListAsync()
+ {
+ var roles = await _appRoleRepository.GetListAsync();
+
+ return roles
+ .Select(r => new AppRoleDto
+ {
+ Id = r.Id,
+ Name = r.Name,
+ Title = r.Title
+ })
+ .ToList();
+ }
+
+ public async Task UpdateTitleAsync(Guid id, string title)
+ {
+ var role = await _appRoleRepository.GetAsync(id);
+
+ role.Title = title;
+
+ await _appRoleRepository.UpdateAsync(role);
+ }
+}
+````
+
+There are some **limitations** of creating a new entity and mapping it to a table of a depended module:
+
+* Your **custom properties must be nullable**. For example, `AppRole.Title` was nullable here. Otherwise, Identity module throws exception because it doesn't know and can not fill the Title when it inserts a new role to the database.
+* As a good practice, you should not update the **properties defined by the module**, especially if it requires a business logic. You typically want to manage your own properties.
+
+##### Alternative Approaches
+
+Instead of creating a new entity class to add a custom property, you can use the following approaches.
+
+###### Using the ExtraProperties
+
+All entities derived from the `AggregateRoot ` class can store name-value pairs in their `ExtraProperties` property, which is a `Dictionary` serialized to JSON in the database table. So, you can add values to this dictionary and query again without changing the entity.
+
+For example, you can store query the title Property inside an `IdentityRole` instead of creating a new entity. Example:
+
+````csharp
+public class IdentityRoleExtendingService : ITransientDependency
+{
+ private readonly IIdentityRoleRepository _identityRoleRepository;
+
+ public IdentityRoleExtendingService(IIdentityRoleRepository identityRoleRepository)
+ {
+ _identityRoleRepository = identityRoleRepository;
+ }
+
+ public async Task GetTitleAsync(Guid id)
+ {
+ var role = await _identityRoleRepository.GetAsync(id);
+
+ return role.GetProperty("Title");
+ }
+
+ public async Task SetTitleAsync(Guid id, string newTitle)
+ {
+ var role = await _identityRoleRepository.GetAsync(id);
+
+ role.SetProperty("Title", newTitle);
+
+ await _identityRoleRepository.UpdateAsync(role);
+ }
+}
+````
+
+* `GetProperty` and `SetProperty` methods are shortcuts to get and set a value in the `role.ExtraProperties` dictionary and they are the recommended way to work with the extra properties.
+
+In this way, you can easily attach any type of value to an entity of a depended module. However, there are some drawbacks of this usage:
+
+* All the extra properties are stored as **a single JSON object** in the database. They are not stored as new table fields, as you may expect. Creating database table indexes and using SQL queries against these properties will be harder compared to simple table fields.
+* Property names are strings, so they are **not type safe**. It is recommended to define constants for these kind of properties to prevent typo errors.
+
+###### Creating a New Table
+
+Instead of creating a new entity and mapping to the same table, you can also create **your own table** to store your properties. You typically duplicate some values of the original entity. For example, you can add `Name` field to your own table which is a duplication of the `Name` field in the original table.
+
+In this case, you don't deal with migration problems, however you need to deal with the problems of data duplication. When the duplicated value changes, you should reflect the same change in your table. You can use local or distributed [event bus](Event-Bus.md) to subscribe to the change events for the original entity. This is the recommended way of depending on a microservice's data from another microservice, especially if they have separate physical databases (you can search on the web on data sharing on a microservice design, it is a wide topic to cover here).
+
+#### Discussion of an Alternative Scenario: Every Module Manages Its Own Migration Path
+
+As mentioned before, `.EntityFrameworkCore.DbMigrations` merges all the database mappings of all the modules (plus your application's mappings) to create a unified migration path.
+
+An alternative approach would be to allow each module to have its own migrations to maintain its database tables. While it seems more module in the beginning, it has some important drawbacks:
+
+* **EF Core migration system depends on the DBMS provider**. For example, if a module has created migrations for SQL Server, then you can not use this migration code for MySQL. It is not practical for a module to maintain migrations for all available DBMS providers. Leaving the migration to the application code (as explained in this document) allows you to **choose the DBMS in the application** code.
+* It would be harder or impossible to **share a table** between modules or **re-use a table** of a module in your application. Because EF Core migration system can not handle it and will throw exceptions like "Table XXX is already exists in the database".
+* It would be harder to **customize/enhance** the mapping and the resulting migration code.
+* It would be harder to track and **apply changes** to database when you use multiple modules.
+
+## Using Multiple Databases
+
+The default startup template is organized to use a single database used by all the modules and by your application. However, the ABP Framework and all the pre-built modules are designed so that **they can use multiple databases**. Each module can use its own database or you can group modules into a few databases.
+
+This section will explain how to move Audit Logging, Setting Management and Permission Management module tables to a **second database** while the remaining modules continue to use the main ("Default") database.
+
+The resulting structure will be like the figure below:
+
+
+
+### Change the Connection Strings Section
+
+First step is to change the connection string section inside all the `appsettings.json` files. Initially, it is like that:
+
+````json
+"ConnectionStrings": {
+ "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true"
+}
+````
+
+Change it as shown below:
+
+````json
+"ConnectionStrings": {
+ "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true",
+ "AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true",
+ "AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true",
+ "AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true"
+}
+````
+
+Added **three more connection strings** for the related module to target the `BookStore_SecondDb` database (they are all same). For example, `AbpPermissionManagement` is the connection string for the permission management module.
+
+The `AbpPermissionManagement` is a constant [defined](https://github.com/abpframework/abp/blob/97eaa6ff5a044f503465455c86332e5a277b077a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDbProperties.cs#L11) by the permission management module. ABP Framework [connection string selection system](Connection-Strings.md) selects this connection string for the permission management module if you define. If you don't define, it fallbacks to the `Default` connection string.
+
+### Create a Second Migration Project
+
+Defining the connection strings as explained above is enough **on runtime**. However, `BookStore_SecondDb` database doesn't exist yet. You need to create the database and the tables for the related modules.
+
+Just like the main database, we want to use the EF Core Code First migration system to create and maintain the second database.
+
+An easy way is to create a second project (`.csproj`) for the second migration `DbContext`.
+
+So, create a new **class library project** in your solution named `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` (or name it better if you didn't like it).
+
+The `.csproj` content should be something like that:
+
+````xml
+
+
+
+
+
+ netcoreapp3.1
+ Acme.BookStore.DbMigrationsForSecondDb
+
+
+
+
+
+
+
+
+
+
+
+````
+
+You can just copy & modify the content of the original `.DbMigrations` project. This project references to the `.EntityFrameworkCore` project. **Only difference** is the `RootNamespace` value.
+
+**Add a reference** to this project from the `.Web` project (otherwise, EF Core tooling doesn't allow to use the `Add-Migration` command).
+
+### Create the Second DbMigrationDbContext
+
+Create a new `DbContext` for the migrations and call the **extension methods** of the modules to configure the database tables for the related modules:
+
+````csharp
+[ConnectionStringName("AbpPermissionManagement")]
+public class BookStoreSecondMigrationsDbContext :
+ AbpDbContext
+{
+ public BookStoreSecondMigrationsDbContext(
+ DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+
+ /* Include modules to your migration db context */
+
+ builder.ConfigurePermissionManagement();
+ builder.ConfigureSettingManagement();
+ builder.ConfigureAuditLogging();
+ }
+}
+````
+
+> `[ConnectionStringName(...)]` attribute is important here and tells to the ABP Framework which connection string should be used for this `DbContext`. We've used `AbpPermissionManagement`, but all are the same.
+
+Create a **Design Time Db Factory** class, that is used by the EF Core tooling (by `Add-Migration` and `Update-Database` PCM commands for example):
+
+````csharp
+/* This class is needed for EF Core console commands
+ * (like Add-Migration and Update-Database commands) */
+public class BookStoreSecondMigrationsDbContextFactory
+ : IDesignTimeDbContextFactory
+{
+ public BookStoreSecondMigrationsDbContext CreateDbContext(string[] args)
+ {
+ var configuration = BuildConfiguration();
+
+ var builder = new DbContextOptionsBuilder()
+ .UseSqlServer(configuration.GetConnectionString("AbpPermissionManagement"));
+
+ return new BookStoreSecondMigrationsDbContext(builder.Options);
+ }
+
+ private static IConfigurationRoot BuildConfiguration()
+ {
+ var builder = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile("appsettings.json", optional: false);
+
+ return builder.Build();
+ }
+}
+````
+
+This is similar to the class inside the `.EntityFrameworCore.DbMigrations` project, except this one uses the `AbpPermissionManagement` connection string.
+
+Now, you can open the Package Manager Console, select the `.EntityFrameworkCore.DbMigrationsForSecondDb` project as the default project (make sure the `.Web` project is still the startup project) and run the `Add-Migration "Initial"` and `Update-Database` commands as shown below:
+
+
+
+Now, you should have a new database contains only the tables needed by the related modules:
+
+
+
+### Remove Modules from the Main Database
+
+We've **created a second database** contains tables for the Audit Logging, Permission Management and Setting Management modules. So, we should **delete these tables from the main database**. It is pretty easy.
+
+First, remove the following lines from the `MigrationsDbContext` class (`BookStoreMigrationsDbContext` for this example):
+
+````csharp
+builder.ConfigurePermissionManagement();
+builder.ConfigureSettingManagement();
+builder.ConfigureAuditLogging();
+````
+
+Open the Package Manager Console, select the `.EntityFrameworkCore.DbMigrations` as the Default project (make sure that the `.Web` project is still the startup project) and run the following command:
+
+````
+Add-Migration "Removed_Audit_Setting_Permission_Modules"
+````
+
+This command will create a new migration class as shown below:
+
+````csharp
+public partial class Removed_Audit_Setting_Permission_Modules : Migration
+{
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "AbpAuditLogActions");
+
+ migrationBuilder.DropTable(
+ name: "AbpEntityPropertyChanges");
+
+ migrationBuilder.DropTable(
+ name: "AbpPermissionGrants");
+
+ migrationBuilder.DropTable(
+ name: "AbpSettings");
+
+ migrationBuilder.DropTable(
+ name: "AbpEntityChanges");
+
+ migrationBuilder.DropTable(
+ name: "AbpAuditLogs");
+ }
+
+ ...
+}
+````
+
+Be careful in this step:
+
+* If you have a **live system**, then you should care about the **data loss**. You need to move the table contents to the second database before deleting the tables.
+* If you **haven't started** your project yet, you can consider to **remove all the migrations** and re-create the initial one to have a cleaner migration history.
+
+Run the `Update-Database` command to delete the tables from your main database.
+
+Notice that you've also **deleted some initial seed data** (for example, permission grants for the admin role) if you haven't copied it to the new database. If you run the application, you may not login anymore. The solution is simple: **Re-run the `.DbMigrator` console application** in your solution, it will seed the new database.
+
+### Automate the Second Database Schema Migration
+
+`.DbMigrator` console application can run the database seed code across multiple databases, without any additional configuration. However, it can not run the EF Core Code First Migrations inside the second database migration project. Now, you will see how to configure the console migration application to handle both databases.
+
+#### Implementing the IBookStoreDbSchemaMigrator
+
+`EntityFrameworkCoreBookStoreDbSchemaMigrator` class inside the `Acme.BookStore.EntityFrameworkCore.DbMigrations` project is responsible to migrate the database schema for the `BookStoreMigrationsDbContext`. It should be like that:
+
+````csharp
+[Dependency(ReplaceServices = true)]
+public class EntityFrameworkCoreBookStoreDbSchemaMigrator
+ : IBookStoreDbSchemaMigrator, ITransientDependency
+{
+ private readonly IServiceProvider _serviceProvider;
+
+ public EntityFrameworkCoreBookStoreDbSchemaMigrator(
+ IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public async Task MigrateAsync()
+ {
+ /* We are intentionally resolving the BookStoreMigrationsDbContext
+ * from IServiceProvider (instead of directly injecting it)
+ * to properly get the connection string of the current tenant in the
+ * current scope.
+ */
+
+ await _serviceProvider
+ .GetRequiredService()
+ .Database
+ .MigrateAsync();
+ }
+}
+````
+
+It implements the `IBookStoreDbSchemaMigrator` and **replaces existing services** (see the first line).
+
+Remove the `[Dependency(ReplaceServices = true)]` line, because we will have two implementations of this interface and we want to use both. We don't want to replace one of them.
+
+Create a copy of this class inside the new migration project (`Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb`), but use the `BookStoreSecondMigrationsDbContext`. Example implementation:
+
+````csharp
+public class EntityFrameworkCoreSecondBookStoreDbSchemaMigrator
+ : IBookStoreDbSchemaMigrator, ITransientDependency
+{
+ private readonly IServiceProvider _serviceProvider;
+
+ public EntityFrameworkCoreSecondBookStoreDbSchemaMigrator(
+ IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public async Task MigrateAsync()
+ {
+ /* We are intentionally resolving the BookStoreSecondMigrationsDbContext
+ * from IServiceProvider (instead of directly injecting it)
+ * to properly get the connection string of the current tenant in the
+ * current scope.
+ */
+
+ await _serviceProvider
+ .GetRequiredService()
+ .Database
+ .MigrateAsync();
+ }
+}
+````
+
+> Name of this class is important for [dependency injection](Dependency-Injection.md). It should end with `BookStoreDbSchemaMigrator` to be injectable by `IBookStoreDbSchemaMigrator` reference.
+
+We, now, have two implementations of the `IBookStoreDbSchemaMigrator` interface, each one responsible to migrate the related database schema.
+
+#### Define a Module Class for the Second Migration Project
+
+It is time to define the [module](Module-Development-Basics.md) class for this second migrations (`Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb`) project:
+
+````csharp
+[DependsOn(
+ typeof(BookStoreEntityFrameworkCoreModule)
+ )]
+public class BookStoreEntityFrameworkCoreSecondDbMigrationsModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddAbpDbContext();
+ }
+}
+````
+
+Now, reference `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` project from the `Acme.BookStore.DbMigrator` project and `typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule)` to the dependency list of the `BookStoreDbMigratorModule`. `BookStoreDbMigratorModule` class should be something like that:
+
+````csharp
+[DependsOn(
+ typeof(AbpAutofacModule),
+ typeof(BookStoreEntityFrameworkCoreDbMigrationsModule),
+ typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule), // ADDED THIS!
+ typeof(BookStoreApplicationContractsModule)
+ )]
+public class BookStoreDbMigratorModule : AbpModule
+{
+ ...
+}
+````
+
+We had a reference to the `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` project from the `Acme.BookStore.Web` project, but hadn't added module dependency since we hadn't created it before. But, now we have it and we need to add `typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule)` to the dependency list of the `BookStoreWebModule` class.
+
+#### Run the Database Migrator!
+
+You can run the `.DbMigrator` application to migrate & seed the databases. To test, you can delete both databases and run the `.DbMigrator` application again to see if it creates both of the databases.
+
+## Conclusion
+
+This document explains how to split your databases and manage your database migrations of your solution for Entity Framework Core. In brief, you need to have a separate migration project per different databases.
+
+## Source Code
+
+You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp/tree/dev/samples/EfCoreMigrationDemo). However, you need to read and understand this document in order to understand the example project's source code.
\ No newline at end of file
diff --git a/docs/en/Entity-Framework-Core-Other-DBMS.md b/docs/en/Entity-Framework-Core-Other-DBMS.md
index 854309d070..a08ac6d169 100644
--- a/docs/en/Entity-Framework-Core-Other-DBMS.md
+++ b/docs/en/Entity-Framework-Core-Other-DBMS.md
@@ -1,12 +1,16 @@
-# Switch to another DBMS for Entity Framework Core
+# Switch to Another DBMS for Entity Framework Core
-**[The application startup template](Startup-Templates/Application.md)** comes with SQL Server provider pre-configured for the Entity Framework Core. Entity Framework Core supports [many other DBMSs](https://docs.microsoft.com/en-us/ef/core/providers/) and you can use any of them with your ABP based applications.
+**[The application startup template](Startup-Templates/Application.md)** comes with **SQL Server provider pre-configured** for the Entity Framework Core. However, EF Core supports [many other DBMSs](https://docs.microsoft.com/en-us/ef/core/providers/) and you can use any of them within your ABP based applications.
-ABP framework provides integration packages for some common DBMSs to make the configuration a bit easier (see the [entity framework core document](Entity-Framework-Core.md) for a list of available integration packages). However, you can configure your DBMS provider without these integration packages.
+ABP framework provides **integration packages** for some common DBMSs to make the configuration a bit easier. You can use the following documents to learn how to **switch to your favorite DBMS**:
-While using the integration package is always recommended (it also makes standard for the depended version across different modules), you can do it manually if there is no integration package for your DBMS provider.
+* [MySQL](Entity-Framework-Core-MySQL.md)
+* [PostgreSQL](Entity-Framework-Core-PostgreSQL.md)
+* [SQLite](Entity-Framework-Core-SQLite.md)
-This document explains how to switch to MySQL without using [the MySQL integration package](Entity-Framework-Core-MySQL.md).
+However, you can configure your DBMS provider **without** these integration packages. While using the integration package is always recommended (it also makes standard for the depended version across different modules), you can do it manually if there is no integration package for your DBMS provider.
+
+For an example, this document explains how to switch to MySQL without using [the MySQL integration package](Entity-Framework-Core-MySQL.md).
## Replace the SQL Server Dependency
diff --git a/docs/en/Entity-Framework-Core.md b/docs/en/Entity-Framework-Core.md
index 586c8d4a01..788f3234ed 100644
--- a/docs/en/Entity-Framework-Core.md
+++ b/docs/en/Entity-Framework-Core.md
@@ -32,14 +32,9 @@ namespace MyCompany.MyProject
Entity Framework Core supports various database management systems ([see all](https://docs.microsoft.com/en-us/ef/core/providers/)). ABP framework and this document doesn't depend on any specific DBMS.
-If you are creating a reusable library, avoid to depend on a specific DBMS package. However, in a final application you eventually will select a DBMS.
+If you are creating a [reusable application module](Modules/Index.md), avoid to depend on a specific DBMS package. However, in a final application you eventually will select a DBMS.
-ABP framework provides integration packages for some common DBMSs to make the configuration a bit easier. [The startup templates](Startup-Templates/Index.md) come with **SQL Server (localdb) pre-configured**. See the following documents to learn how to configure for the other DBMS providers:
-
-* [MySQL](Entity-Framework-Core-MySQL.md)
-* [PostgreSQL](Entity-Framework-Core-PostgreSQL.md)
-* [SQLite](Entity-Framework-Core-SQLite.md)
-* [Others](Entity-Framework-Core-Other-DBMS.md)
+See [Switch to Another DBMS for Entity Framework Core](Entity-Framework-Core-Other-DBMS.md) document to learn how to switch the DBMS.
## Creating DbContext
diff --git a/docs/en/Exception-Handling.md b/docs/en/Exception-Handling.md
index 93bad2b6ed..0e0a530416 100644
--- a/docs/en/Exception-Handling.md
+++ b/docs/en/Exception-Handling.md
@@ -204,7 +204,7 @@ Instead of localizing the message while throwing the exception, you can separate
First, define the **code-namespace** to **localization resource** mapping in the module configuration:
````C#
-services.Configure(options =>
+services.Configure(options =>
{
options.MapCodeNamespace("Volo.Qa", typeof(QaResource));
});
@@ -285,7 +285,7 @@ The `IHttpExceptionStatusCodeFinder` is used to automatically determine the HTTP
Automatic HTTP status code determination can be overrided by custom mappings. For example:
````C#
-services.Configure(options =>
+services.Configure(options =>
{
options.Map("Volo.Qa:010002", HttpStatusCode.Conflict);
});
diff --git a/docs/en/Getting-Started-Angular-Template.md b/docs/en/Getting-Started-Angular-Template.md
index d6d3b84be8..9a4fddcf2a 100644
--- a/docs/en/Getting-Started-Angular-Template.md
+++ b/docs/en/Getting-Started-Angular-Template.md
@@ -1,6 +1,6 @@
## Getting Started With the Angular Application Template
-This tutorial explain how to create a new Angular application using the startup template, configure and run it.
+This tutorial explains how to create a new Angular application using the startup template, configure and run it.
### Creating a New Project
@@ -103,7 +103,7 @@ Most of the application APIs require authentication & authorization. If you want
#### Run the Angular Application (Client Side)
-Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com) package manager while npm install will also work in most cases):
+Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest the [yarn](https://yarnpkg.com) package manager while `npm install` will also work in most cases)
````bash
yarn
@@ -123,4 +123,4 @@ The startup template includes the **identity management** and **tenant managemen
### What's Next?
-* [Application development tutorial](Tutorials/Angular/Part-I.md)
+* [Application development tutorial](Tutorials/Part-1)
diff --git a/docs/en/Getting-Started-AspNetCore-MVC-Template.md b/docs/en/Getting-Started-AspNetCore-MVC-Template.md
index 563ce0182d..311bb8b4dc 100644
--- a/docs/en/Getting-Started-AspNetCore-MVC-Template.md
+++ b/docs/en/Getting-Started-AspNetCore-MVC-Template.md
@@ -101,4 +101,4 @@ The startup template includes the **identity management** and **tenant managemen
### What's Next?
-* [Application development tutorial](Tutorials/AspNetCore-Mvc/Part-I.md)
+* [Application development tutorial](Tutorials/Part-1.md)
diff --git a/docs/en/Modules/Identity.md b/docs/en/Modules/Identity.md
new file mode 100644
index 0000000000..028144c624
--- /dev/null
+++ b/docs/en/Modules/Identity.md
@@ -0,0 +1,4 @@
+# Identity Management Module
+
+See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon...
+
diff --git a/docs/en/Modules/Tenant-Management.md b/docs/en/Modules/Tenant-Management.md
new file mode 100644
index 0000000000..73e16aee8e
--- /dev/null
+++ b/docs/en/Modules/Tenant-Management.md
@@ -0,0 +1,3 @@
+# Tenant Management Module
+
+TODO
\ No newline at end of file
diff --git a/docs/en/Multi-Tenancy.md b/docs/en/Multi-Tenancy.md
index 26b28b0ad7..de5e62f688 100644
--- a/docs/en/Multi-Tenancy.md
+++ b/docs/en/Multi-Tenancy.md
@@ -302,6 +302,7 @@ TODO:...
Volo.Abp.AspNetCore.MultiTenancy package adds following tenant resolvers to determine current tenant from current web request (ordered by priority). These resolvers are added and work out of the box:
+* **CurrentUserTenantResolveContributor**: Gets the tenant id from claims of the current user, if the current user has logged in. **This should always be stay as the first contributor for security**.
* **QueryStringTenantResolver**: Tries to find current tenant id from query string parameter. Parameter name is "__tenant" by default.
* **RouteTenantResolver**: Tries to find current tenant id from route (URL path). Variable name is "__tenant" by default. So, if you defined a route with this variable, then it can determine the current tenant from the route.
* **HeaderTenantResolver**: Tries to find current tenant id from HTTP header. Header name is "__tenant" by default.
diff --git a/docs/en/Samples/Microservice-Demo.md b/docs/en/Samples/Microservice-Demo.md
index ba32878ea6..f074c57e51 100644
--- a/docs/en/Samples/Microservice-Demo.md
+++ b/docs/en/Samples/Microservice-Demo.md
@@ -419,7 +419,7 @@ A screenshot from the Products page:
#### Using Microservices
-Publc web site application uses the Blogging and Product microservices for all operations, over the Public Web Site Gateway (PublicWebSiteGateway.Host).
+Public web site application uses the Blogging and Product microservices for all operations, over the Public Web Site Gateway (PublicWebSiteGateway.Host).
##### Remote End Point
diff --git a/docs/en/Startup-Templates/Application.md b/docs/en/Startup-Templates/Application.md
index 1fbf1be8e2..d25feb490a 100644
--- a/docs/en/Startup-Templates/Application.md
+++ b/docs/en/Startup-Templates/Application.md
@@ -5,8 +5,8 @@
This template provides a layered application structure based on the [Domain Driven Design](../Domain-Driven-Design.md) (DDD) practices. This document explains the solution structure and projects in details. If you want to start quickly, follow the guides below:
* See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template (uses MVC as the UI framework and Entity Framework Core as the database provider).
-* See the [ASP.NET Core MVC Application Development Tutorial](../Tutorials/AspNetCore-Mvc/Part-I.md) to learn how to develop applications using this template (uses MVC as the UI framework and Entity Framework Core as the database provider).
-* See the [Angular Application Development Tutorial](../Tutorials/Angular/Part-I.md) to learn how to develop applications using this template (uses Angular as the UI framework and MongoDB as the database provider).
+* See the [ASP.NET Core MVC Application Development Tutorial](../Tutorials/Part-1.md?UI=MVC) to learn how to develop applications using this template (uses MVC as the UI framework and Entity Framework Core as the database provider).
+* See the [Angular Application Development Tutorial](../Tutorials/Part-1.md?UI=NG) to learn how to develop applications using this template (uses Angular as the UI framework and MongoDB as the database provider).
## How to Start With?
@@ -270,4 +270,4 @@ The files under the `angular/src/environments` folder has the essential configur
## What's Next?
- See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template.
-- See the [ASP.NET Core MVC Tutorial](../Tutorials/AspNetCore-Mvc/Part-I.md) to learn how to develop applications using this template.
+- See the [ASP.NET Core MVC Tutorial](../Tutorials/Part-1.md?UI=MVC) to learn how to develop applications using this template.
diff --git a/docs/en/Tutorials/Angular/Part-I.md b/docs/en/Tutorials/Angular/Part-I.md
index 6d5f91bce7..65a7dc5714 100644
--- a/docs/en/Tutorials/Angular/Part-I.md
+++ b/docs/en/Tutorials/Angular/Part-I.md
@@ -1,659 +1,6 @@
-## Angular Tutorial - Part I
+# Tutorials
-### About this Tutorial
+## Application Development
-In this tutorial series, you will build an application that is used to manage a list of books & their authors. **Angular** will be used as the UI framework and **MongoDB** will be used as the database provider.
-
-This is the first part of the Angular tutorial series. See all parts:
-
-- **Part I: Create the project and a book list page (this tutorial)**
-- [Part II: Create, Update and Delete books](Part-II.md)
-- [Part III: Integration Tests](Part-III.md)
-
-You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb).
-
-### Creating the Project
-
-Create a new project named `Acme.BookStore` by selecting the Angular as the UI framework and MongoDB as the database provider, create the database and run the application by following the [Getting Started document](../../Getting-Started-Angular-Template.md).
-
-### Solution Structure (Backend)
-
-This is how the layered solution structure looks after it's created:
-
-
-
-> You can see the [Application template document](../../Startup-Templates/Application.md) to understand the solution structure in details. However, you will understand the basics with this tutorial.
-
-### Create the Book Entity
-
-Domain layer in the startup template is separated into two projects:
-
-- `Acme.BookStore.Domain` contains your [entities](../../Entities.md), [domain services](../../Domain-Services.md) and other core domain objects.
-- `Acme.BookStore.Domain.Shared` contains constants, enums or other domain related objects those can be shared with clients.
-
-Define [entities](../../Entities.md) in the **domain layer** (`Acme.BookStore.Domain` project) of the solution. The main entity of the application is the `Book`. Create a class, named `Book`, in the `Acme.BookStore.Domain` project as shown below:
-
-```C#
-using System;
-using Volo.Abp.Domain.Entities.Auditing;
-
-namespace Acme.BookStore
-{
- public class Book : AuditedAggregateRoot
- {
- public string Name { get; set; }
-
- public BookType Type { get; set; }
-
- public DateTime PublishDate { get; set; }
-
- public float Price { get; set; }
- }
-}
-```
-
-- ABP has two fundamental base classes for entities: `AggregateRoot` and `Entity`. **Aggregate Root** is one of the **Domain Driven Design (DDD)** concepts. See [entity document](../../Entities.md) for details and best practices.
-- `Book` entity inherits `AuditedAggregateRoot` which adds some auditing properties (`CreationTime`, `CreatorId`, `LastModificationTime`... etc.) on top of the `AggregateRoot` class.
-- `Guid` is the **primary key type** of the `Book` entity.
-
-#### BookType Enum
-
-Define the `BookType` enum in the `Acme.BookStore.Domain.Shared` project:
-
-```C#
-namespace Acme.BookStore
-{
- public enum BookType
- {
- Undefined,
- Adventure,
- Biography,
- Dystopia,
- Fantastic,
- Horror,
- Science,
- ScienceFiction,
- Poetry
- }
-}
-```
-
-#### Add Book Entity to Your DbContext
-
-Add a `IMongoCollection` property to the `BookStoreMongoDbContext` inside the `Acme.BookStore.MongoDB` project:
-
-```csharp
-public class BookStoreMongoDbContext : AbpMongoDbContext
-{
- public IMongoCollection Books => Collection();
- ...
-}
-```
-
-#### Add Seed (Sample) Data
-
-This section is optional, but it would be good to have an initial data in the database in the first run. ABP provides a [data seed system](../../Data-Seeding.md). Create a class deriving from the `IDataSeedContributor` in the `.Domain` project:
-
-```csharp
-using System;
-using System.Threading.Tasks;
-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;
-
- public BookStoreDataSeederContributor(IRepository bookRepository)
- {
- _bookRepository = bookRepository;
- }
-
- public async Task SeedAsync(DataSeedContext context)
- {
- if (await _bookRepository.GetCountAsync() > 0)
- {
- return;
- }
-
- await _bookRepository.InsertAsync(
- new Book
- {
- Name = "1984",
- Type = BookType.Dystopia,
- PublishDate = new DateTime(1949, 6, 8),
- Price = 19.84f
- }
- );
-
- 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
- }
- );
- }
- }
-}
-
-```
-
-`BookStoreDataSeederContributor` simply inserts two books into database if there is no book added before. ABP automatically discovers and executes this class when you seed the database by running the `Acme.BookStore.DbMigrator` project.
-
-### Create the Application Service
-
-The next step is to create an [application service](../../Application-Services.md) to manage (create, list, update, delete...) the books. Application layer in the startup template is separated into two projects:
-
-- `Acme.BookStore.Application.Contracts` mainly contains your DTOs and application service interfaces.
-- `Acme.BookStore.Application` contains the implementations of your application services.
-
-#### BookDto
-
-Create a DTO class named `BookDto` into the `Acme.BookStore.Application.Contracts` project:
-
-```C#
-using System;
-using Volo.Abp.Application.Dtos;
-
-namespace Acme.BookStore
-{
- public class BookDto : AuditedEntityDto
- {
- public string Name { get; set; }
-
- public BookType Type { get; set; }
-
- public DateTime PublishDate { get; set; }
-
- public float Price { get; set; }
- }
-}
-```
-
-- **DTO** classes are used to **transfer data** between the _presentation layer_ and the _application layer_. See the [Data Transfer Objects document](../../Data-Transfer-Objects.md) for more details.
-- `BookDto` is used to transfer book data to the presentation layer in order to show the book information on the UI.
-- `BookDto` is derived from the `AuditedEntityDto` which has audit properties just like the `Book` class defined above.
-
-It will be needed to convert `Book` entities to `BookDto` objects while returning books to the presentation layer. [AutoMapper](https://automapper.org) library can automate this conversion when you define the proper mapping. Startup template comes with AutoMapper configured, so you can just define the mapping in the `BookStoreApplicationAutoMapperProfile` class in the `Acme.BookStore.Application` project:
-
-```csharp
-using AutoMapper;
-
-namespace Acme.BookStore
-{
- public class BookStoreApplicationAutoMapperProfile : Profile
- {
- public BookStoreApplicationAutoMapperProfile()
- {
- CreateMap();
- }
- }
-}
-```
-
-#### CreateUpdateBookDto
-
-Create a DTO class named `CreateUpdateBookDto` into the `Acme.BookStore.Application.Contracts` project:
-
-```c#
-using System;
-using System.ComponentModel.DataAnnotations;
-
-namespace Acme.BookStore
-{
- public class CreateUpdateBookDto
- {
- [Required]
- [StringLength(128)]
- public string Name { get; set; }
-
- [Required]
- public BookType Type { get; set; } = BookType.Undefined;
-
- [Required]
- public DateTime PublishDate { get; set; }
-
- [Required]
- public float Price { get; set; }
- }
-}
-```
-
-- This DTO class is used to get book information from the user interface while creating or updating a book.
-- It defines data annotation attributes (like `[Required]`) to define validations for the properties. DTOs are [automatically validated](../../Validation.md) by the ABP framework.
-
-Next, add a mapping in `BookStoreApplicationAutoMapperProfile` from the `CreateUpdateBookDto` object to the `Book` entity:
-
-```csharp
-CreateMap();
-```
-
-#### IBookAppService
-
-Define an interface named `IBookAppService` in the `Acme.BookStore.Application.Contracts` project:
-
-```C#
-using System;
-using Volo.Abp.Application.Dtos;
-using Volo.Abp.Application.Services;
-
-namespace Acme.BookStore
-{
- public interface IBookAppService :
- ICrudAppService< //Defines CRUD methods
- BookDto, //Used to show books
- Guid, //Primary key of the book entity
- PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books
- CreateUpdateBookDto, //Used to create a new book
- CreateUpdateBookDto> //Used to update a book
- {
-
- }
-}
-```
-
-- Defining interfaces for application services is not required by the framework. However, it's suggested as a best practice.
-- `ICrudAppService` defines common **CRUD** methods: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync` and `DeleteAsync`. It's not required to extend it. Instead, you could inherit from the empty `IApplicationService` interface and define your own methods manually.
-- There are some variations of the `ICrudAppService` where you can use separated DTOs for each method.
-
-#### BookAppService
-
-Implement the `IBookAppService` as named `BookAppService` in the `Acme.BookStore.Application` project:
-
-```C#
-using System;
-using Volo.Abp.Application.Dtos;
-using Volo.Abp.Application.Services;
-using Volo.Abp.Domain.Repositories;
-
-namespace Acme.BookStore
-{
- public class BookAppService :
- CrudAppService,
- IBookAppService
- {
- public BookAppService(IRepository repository)
- : base(repository)
- {
-
- }
- }
-}
-```
-
-- `BookAppService` is derived from `CrudAppService<...>` which implements all the CRUD methods defined above.
-- `BookAppService` injects `IRepository` which is the default repository for the `Book` entity. ABP automatically creates default repositories for each aggregate root (or entity). See the [repository document](../../Repositories.md).
-- `BookAppService` uses `IObjectMapper` to convert `Book` objects to `BookDto` objects and `CreateUpdateBookDto` objects to `Book` objects. The Startup template uses the [AutoMapper](http://automapper.org/) library as the object mapping provider. You defined the mappings before, so it will work as expected.
-
-### Auto API Controllers
-
-You normally create **Controllers** to expose application services as **HTTP API** endpoints. Thus allowing browser or 3rd-party clients to call them via AJAX. ABP can [**automagically**](../../AspNetCore/Auto-API-Controllers.md) configures your application services as MVC API Controllers by convention.
-
-#### Swagger UI
-
-The startup template is configured to run the [swagger UI](https://swagger.io/tools/swagger-ui/) using the [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) library. Run the `Acme.BookStore.HttpApi.Host` application and enter `https://localhost:XXXX/swagger/` (replace XXXX by your own port) as URL on your browser.
-
-You will see some built-in service endpoints as well as the `Book` service and its REST-style endpoints:
-
-
-
-Swagger has a nice UI to test APIs. You can try to execute the `[GET] /api/app/book` API to get a list of books.
-
-### Create the Books Page
-
-In this tutorial;
-
-- [Angular CLI](https://angular.io/cli) will be used to create modules, components and services
-- [NGXS](https://ngxs.gitbook.io/ngxs/) will be used as the state management library
-- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) will be used as the UI component library.
-- [Visual Studio Code](https://code.visualstudio.com/) will be used as the code editor (you can use your favorite editor).
-
-#### Install NPM Packages
-
-Open a terminal window and go to `angular` folder and then run `yarn` command for installing NPM packages:
-
-```
-yarn
-```
-
-#### BooksModule
-
-Run the following command line to create a new module, named `BooksModule`:
-
-```bash
-yarn ng generate module books --route books --module app.module
-```
-
-
-
-Run `yarn start`, wait Angular to run the application and open `http://localhost:4200/books` on a browser:
-
-
-
-#### Routing
-
-Open the `app-routing.module.ts` and replace `books` as shown below:
-
-```js
-import { ApplicationLayoutComponent } from '@abp/ng.theme.basic';
-
-//...
-{
- path: 'books',
- component: ApplicationLayoutComponent,
- loadChildren: () => import('./books/books.module').then(m => m.BooksModule),
- data: {
- routes: {
- name: 'Books',
- } as ABP.Route,
- },
-},
-```
-
-`ApplicationLayoutComponent` configuration sets the application layout to the new page. If you would like to see your route on the navigation bar (main menu) you must also add the `data` object with `name` property in your route.
-
-
-
-#### Book List Component
-
-First, replace the `books.component.html` to the following line to place the router-outlet:
-
-```html
-
-```
-
-Then run the command below on the terminal in the root folder to generate a new component, named book-list:
-
-```bash
-yarn ng generate component books/book-list
-```
-
-
-
-Import the `SharedModule` to the `BooksModule` to reuse some components and services defined in:
-
-```js
-import { SharedModule } from '../shared/shared.module';
-
-@NgModule({
- //...
- imports: [
- //...
- SharedModule,
- ],
-})
-export class BooksModule {}
-```
-
-Then, update the `routes` in the `books-routing.module.ts` to add the new book-list component:
-
-```js
-import { BookListComponent } from './book-list/book-list.component';
-
-const routes: Routes = [
- {
- path: '',
- component: BooksComponent,
- children: [{ path: '', component: BookListComponent }],
- },
-];
-
-@NgModule({
- imports: [RouterModule.forChild(routes)],
- exports: [RouterModule],
-})
-export class BooksRoutingModule {}
-```
-
-
-
-#### Create BooksState
-
-Run the following command in the terminal to create a new state, named `BooksState`:
-
-```shell
-yarn ng generate ngxs-schematic:state books
-```
-
-This command creates several new files and edits `app.modules.ts` to import the `NgxsModule` with the new state:
-
-```js
-// app.module.ts
-
-import { BooksState } from './store/states/books.state';
-
-@NgModule({
- imports: [
- //...
- NgxsModule.forRoot([BooksState]),
- ],
- //...
-})
-export class AppModule {}
-```
-
-#### Get Books Data from Backend
-
-First, create data types to map data returning from the backend (you can check swagger UI or your backend API to know the data format).
-
-Modify the `books.ts` as shown below:
-
-```js
-export namespace Books {
- export interface State {
- books: Response;
- }
-
- export interface Response {
- items: Book[];
- totalCount: number;
- }
-
- export interface Book {
- name: string;
- type: BookType;
- publishDate: string;
- price: number;
- lastModificationTime: string;
- lastModifierId: string;
- creationTime: string;
- creatorId: string;
- id: string;
- }
-
- export enum BookType {
- Undefined,
- Adventure,
- Biography,
- Dystopia,
- Fantastic,
- Horror,
- Science,
- ScienceFiction,
- Poetry,
- }
-}
-```
-
-Added `Book` interface that represents a book object and `BookType` enum represents a book category.
-
-#### BooksService
-
-Now, create a new service, named `BooksService` to perform HTTP calls to the server:
-
-```bash
-yarn ng generate service books/shared/books
-```
-
-
-
-Modify `books.service.ts` as shown below:
-
-```js
-import { Injectable } from '@angular/core';
-import { RestService } from '@abp/ng.core';
-import { Books } from '../../store/models';
-import { Observable } from 'rxjs';
-
-@Injectable({
- providedIn: 'root',
-})
-export class BooksService {
- constructor(private restService: RestService) {}
-
- get(): Observable {
- return this.restService.request({
- method: 'GET',
- url: '/api/app/book'
- });
- }
-}
-```
-
-Added the `get` method to get the list of books by performing an HTTP request to the related endpoint.
-
-Replace `books.actions.ts` content as shown below:
-
-```js
-export class GetBooks {
- static readonly type = '[Books] Get';
-}
-```
-
-#### Implement the BooksState
-
-Open the `books.state.ts` and change the file as shown below:
-
-```js
-import { State, Action, StateContext, Selector } from '@ngxs/store';
-import { GetBooks } from '../actions/books.actions';
-import { Books } from '../models/books';
-import { BooksService } from '../../books/shared/books.service';
-import { tap } from 'rxjs/operators';
-
-@State({
- name: 'BooksState',
- defaults: { books: {} } as Books.State,
-})
-export class BooksState {
- @Selector()
- static getBooks(state: Books.State) {
- return state.books.items || [];
- }
-
- constructor(private booksService: BooksService) {}
-
- @Action(GetBooks)
- get(ctx: StateContext) {
- return this.booksService.get().pipe(
- tap(booksResponse => {
- ctx.patchState({
- books: booksResponse,
- });
- }),
- );
- }
-}
-```
-
-Added the `GetBooks` action that uses the `BookService` defined above to get the books and patch the state.
-
-> NGXS requires to return the observable without subscribing it, as done in this sample (in the get function).
-
-#### BookListComponent
-
-Modify the `book-list.component.ts` as shown below:
-
-```js
-import { Component, OnInit } from '@angular/core';
-import { Store, Select } from '@ngxs/store';
-import { BooksState } from '../../store/states';
-import { Observable } from 'rxjs';
-import { Books } from '../../store/models';
-import { GetBooks } from '../../store/actions';
-
-@Component({
- selector: 'app-book-list',
- templateUrl: './book-list.component.html',
- styleUrls: ['./book-list.component.scss'],
-})
-export class BookListComponent implements OnInit {
- @Select(BooksState.getBooks)
- books$: Observable;
-
- booksType = Books.BookType;
-
- loading = false;
-
- constructor(private store: Store) {}
-
- ngOnInit() {
- this.loading = true;
- this.store.dispatch(new GetBooks()).subscribe(() => {
- this.loading = false;
- });
- }
-}
-```
-
-> See the [Dispatching Actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions) and [Select](https://ngxs.gitbook.io/ngxs/concepts/select) on the NGXS documentation for more information on these NGXS features.
-
-Replace `book-list.component.html` content as shown below:
-
-```html
-
-
-
-
-
- Books
-
-
-
-
-
-
-
-
-
Book name
-
Book type
-
Publish date
-
Price
-
-
-
-
-
{%{{{ data.name }}}%}
-
{%{{{ booksType[data.type] }}}%}
-
{%{{{ data.publishDate | date }}}%}
-
{%{{{ data.price }}}%}
-
-
-
-
-
-```
-
-> We've used [PrimeNG table](https://www.primefaces.org/primeng/#/table) in this component.
-
-The resulting books page is shown below:
-
-
-
-And this is the folder & file structure by the end of this tutorial:
-
-
-
-> This tutorial follows the [Angular Style Guide](https://angular.io/guide/styleguide#file-tree).
-
-### Next Part
-
-See the [next part](Part-II.md) of this tutorial.
+* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
+* [With Angular UI](../Part-1?UI=NG)
diff --git a/docs/en/Tutorials/Angular/Part-II.md b/docs/en/Tutorials/Angular/Part-II.md
index 6d1c563600..65a7dc5714 100644
--- a/docs/en/Tutorials/Angular/Part-II.md
+++ b/docs/en/Tutorials/Angular/Part-II.md
@@ -1,587 +1,6 @@
-## Angular Tutorial - Part II
+# Tutorials
-### About this Tutorial
+## Application Development
-This is the second part of the Angular tutorial series. See all parts:
-
-- [Part I: Create the project and a book list page](Part-I.md)
-- **Part II: Create, Update and Delete books (this tutorial)**
-- [Part III: Integration Tests](Part-III.md)
-
-You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb).
-
-### Creating a New Book
-
-In this section, you will learn how to create a new modal dialog form to create a new book.
-
-#### Type Definition
-
-Create an interface, named `CreateUpdateBookInput` in the `books.ts` as shown below:
-
-```js
-export namespace Books {
- //...
- export interface CreateUpdateBookInput {
- name: string;
- type: BookType;
- publishDate: string;
- price: number;
- }
-}
-```
-
-`CreateUpdateBookInput` interface matches the `CreateUpdateBookDto` in the backend.
-
-#### Service Method
-
-Open the `books.service.ts` and add a new method, named `create` to perform an HTTP POST request to the server:
-
-```js
-create(createBookInput: Books.CreateUpdateBookInput): Observable {
- return this.restService.request({
- method: 'POST',
- url: '/api/app/book',
- body: createBookInput
- });
-}
-```
-
-- `restService.request` function gets generic parameters for the types sent to and received from the server. This example sends a `CreateUpdateBookInput` object and receives a `Book` object (you can set `void` for request or return type if not used).
-
-#### State Definitions
-
-Add the `CreateUpdateBook` action to the `books.actions.ts` as shown below:
-
-```js
-import { Books } from '../models';
-
-export class CreateUpdateBook {
- static readonly type = '[Books] Create Update Book';
- constructor(public payload: Books.CreateUpdateBookInput) {}
-}
-```
-
-Open `books.state.ts` and define the `save` method that will listen to a `CreateUpdateBook` action to create a book:
-
-```js
-import { ... , CreateUpdateBook } from '../actions/books.actions';
-import { ... , switchMap } from 'rxjs/operators';
-//...
-@Action(CreateUpdateBook)
-save(ctx: StateContext, action: CreateUpdateBook) {
- return this.booksService
- .create(action.payload)
- .pipe(switchMap(() => ctx.dispatch(new GetBooks())));
-}
-```
-
-When the `SaveBook` action dispatched, the save method is executed. It call `create` method of the `BooksService` defined before. After the service call, `BooksState` dispatches the `GetBooks` action to get books again from the server to refresh the page.
-
-#### Add a Modal to BookListComponent
-
-Open the `book-list.component.html` and add the `abp-modal` to show/hide the modal to create a new book.
-
-```html
-
-
-
New Book
-
-
-
-
-
-
-
-
-```
-
-`abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits.
-
-Add a button, labeled `New book` to show the modal:
-
-```html
-
-
-
- Books
-
-
-
-
-
-
-```
-
-Open the `book-list.component.ts` and add `isModalOpen` variable and `createBook` method to show/hide the modal.
-
-```js
-isModalOpen = false;
-
-//...
-
-createBook() {
- this.isModalOpen = true;
-}
-```
-
-
-
-#### Create a Reactive Form
-
-> [Reactive forms](https://angular.io/guide/reactive-forms) provide a model-driven approach to handling form inputs whose values change over time.
-
-Add a `form` variable and inject a `FormBuilder` service to the `book-list.component.ts` as shown below (remember add the import statement).
-
-```js
-import { FormGroup, FormBuilder, Validators } from '@angular/forms';
-
-form: FormGroup;
-
-constructor(
- //...
- private fb: FormBuilder
-) {}
-```
-
-> The [FormBuilder](https://angular.io/api/forms/FormBuilder) service provides convenient methods for generating controls. It reduces the amount of boilerplate needed to build complex forms.
-
-Add the `buildForm` method to create book form.
-
-```js
-buildForm() {
- this.form = this.fb.group({
- name: ['', Validators.required],
- type: [null, Validators.required],
- publishDate: [null, Validators.required],
- price: [null, Validators.required],
- });
-}
-```
-
-- The `group` method of `FormBuilder` (`fb`) creates a `FormGroup`.
-- Added `Validators.required` static method that validates the related form element.
-
-Modify the `createBook` method as shown below:
-
-```js
-createBook() {
- this.buildForm();
- this.isModalOpen = true;
-}
-```
-
-#### Create the DOM Elements of the Form
-
-Open `book-list.component.html` and add the form in the body template of the modal.
-
-```html
-
-
-
-```
-
-- This template creates a form with Name, Price, Type and Publish date fields.
-
-> We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component.
-
-#### Datepicker Requirements
-
-You need to import `NgbDatepickerModule` to the `books.module.ts`:
-
-```js
-import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap';
-
-@NgModule({
- imports: [
- // ...
- NgbDatepickerModule,
- ],
-})
-export class BooksModule {}
-```
-
-Then open the `book-list.component.ts` and add `providers` as shown below:
-
-```js
-import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
-
-@Component({
- // ...
- providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
-})
-export class BookListComponent implements OnInit {
-// ...
-```
-
-> The `NgbDateAdapter` converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details.
-
-#### Create the Book Type Array
-
-Open the `book-list.component.ts` and then create an array, named `bookTypeArr`:
-
-```js
-//...
-booksType = Books.BookType;
-
-bookTypeArr = Object.keys(Books.BookType).filter(
- bookType => typeof this.booksType[bookType] === 'number'
-);
-```
-
-The `bookTypeArr` contains the fields of the `BookType` enum. Resulting array is shown below:
-
-```js
-['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...]
-```
-
-This array was used in the previous form template (in the `ngFor` loop).
-
-
-
-
-#### Saving the Book
-
-Open the `book-list.component.html` and add an `abp-button` to save the form.
-
-```html
-
-
-
-
-```
-
-This adds a save button to the bottom area of the modal:
-
-
-
-Then define a `save` method in the `BookListComponent`:
-
-```js
-//...
-import { ..., CreateUpdateBook } from '../../store/actions';
-//...
-save() {
- if (this.form.invalid) {
- return;
- }
-
- this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => {
- this.isModalOpen = false;
- this.form.reset();
- });
-}
-```
-
-### Updating An Existing Book
-
-#### BooksService
-
-Open the `books.service.ts` and then add the `getById` and `update` methods.
-
-```js
-getById(id: string): Observable {
- return this.restService.request({
- method: 'GET',
- url: `/api/app/book/${id}`
- });
-}
-
-update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable {
- return this.restService.request({
- method: 'PUT',
- url: `/api/app/book/${id}`,
- body: updateBookInput
- });
-}
-```
-
-#### CreateUpdateBook Action
-
-Open the `books.actions.ts` and add `id` parameter to the `CreateUpdateBook` action:
-
-```js
-export class CreateUpdateBook {
- static readonly type = '[Books] Create Update Book';
- constructor(public payload: Books.CreateUpdateBookInput, public id?: string) {}
-}
-```
-
-Open `books.state.ts` and modify the `save` method as show below:
-
-```js
-@Action(CreateUpdateBook)
-save(ctx: StateContext, action: CreateUpdateBook) {
- let request;
-
- if (action.id) {
- request = this.booksService.update(action.payload, action.id);
- } else {
- request = this.booksService.create(action.payload);
- }
-
- return request.pipe(switchMap(() => ctx.dispatch(new GetBooks())));
-}
-```
-
-#### BookListComponent
-
-Inject `BooksService` dependency by adding it to the `book-list.component.ts` constructor and add a variable named `selectedBook`.
-
-```js
-import { BooksService } from '../shared/books.service';
-//...
-selectedBook = {} as Books.Book;
-
-constructor(
- //...
- private booksService: BooksService
-)
-```
-
-`booksService` is used to get the editing book to prepare the form. Modify the `buildForm` method to reuse the same form while editing a book.
-
-```js
-buildForm() {
- this.form = this.fb.group({
- name: [this.selectedBook.name || '', Validators.required],
- type: this.selectedBook.type || null,
- publishDate: this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null,
- price: this.selectedBook.price || null,
- });
-}
-```
-
-Add the `editBook` method as shown below:
-
-```js
- editBook(id: string) {
- this.booksService.getById(id).subscribe(book => {
- this.selectedBook = book;
- this.buildForm();
- this.isModalOpen = true;
- });
- }
-```
-
-Added `editBook` method to get the editing book, build the form and show the modal.
-
-Now, add the `selectedBook` definition to `createBook` method to reuse the same form while creating a new book:
-
-```js
- createBook() {
- this.selectedBook = {} as Books.Book;
- //...
- }
-```
-
-Modify the `save` method to pass the id of the selected book as shown below:
-
-```js
-save() {
- if (this.form.invalid) {
- return;
- }
-
- this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id))
- .subscribe(() => {
- this.isModalOpen = false;
- this.form.reset();
- });
-}
-```
-
-#### Add "Actions" Dropdown to the Table
-
-Open the `book-list.component.html` and add modify the `p-table` as shown below:
-
-```html
-
-
-
-
Actions
-
Book name
-
Book type
-
Publish date
-
Price
-
-
-
-
-
-
-
-
-
-
-
-
-
{%{{{ data.name }}}%}
-
{%{{{ booksType[data.type] }}}%}
-
{%{{{ data.publishDate | date }}}%}
-
{%{{{ data.price }}}%}
-
-
-
-```
-
-- Added a `th` for the "Actions" column.
-- Added `button` with `ngbDropdownToggle` to open actions when clicked the button.
-
-> We've used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions.
-
-The final UI looks like:
-
-
-
-Update the modal header to change the title based on the current operation:
-
-```html
-
-
{%{{{ selectedBook.id ? 'Edit' : 'New Book' }}}%}
-
-```
-
-
-
-### Deleting an Existing Book
-
-#### BooksService
-
-Open `books.service.ts` and add a `delete` method to delete a book with the `id` by performing an HTTP request to the related endpoint:
-
-```js
-delete(id: string): Observable {
- return this.restService.request({
- method: 'DELETE',
- url: `/api/app/book/${id}`
- });
-}
-```
-
-#### DeleteBook Action
-
-Add an action named `DeleteBook` to `books.actions.ts`:
-
-```js
-export class DeleteBook {
- static readonly type = '[Books] Delete';
- constructor(public id: string) {}
-}
-```
-
-Open the `books.state.ts` and add the `delete` method that will listen to the `DeleteBook` action to delete a book:
-
-```js
-import { ... , DeleteBook } from '../actions/books.actions';
-//...
-@Action(DeleteBook)
-delete(ctx: StateContext, action: DeleteBook) {
- return this.booksService.delete(action.id).pipe(switchMap(() => ctx.dispatch(new GetBooks())));
-}
-```
-
-- Added `DeleteBook` to the import list.
-- Uses `bookService` to delete the book.
-
-#### Add a Delete Button
-
-Open `book-list.component.html` and modify the `ngbDropdownMenu` to add the delete button as shown below:
-
-```html
-
- ...
-
-
-```
-
-The final actions dropdown UI looks like below:
-
-
-
-#### Delete Confirmation Dialog
-
-Open `book-list.component.ts` and inject the `ConfirmationService`.
-
-```js
-import { ConfirmationService } from '@abp/ng.theme.shared';
-//...
-constructor(
- //...
- private confirmationService: ConfirmationService
-)
-```
-
-> `ConfirmationService` is a simple service provided by ABP framework that internally uses the PrimeNG.
-
-Add a delete method to the `BookListComponent`:
-
-```js
-import { ... , DeleteBook } from '../../store/actions';
-import { ... , Toaster } from '@abp/ng.theme.shared';
-//...
-delete(id: string, name: string) {
- this.confirmationService
- .error(`${name} will be deleted. Do you confirm that?`, 'Are you sure?')
- .subscribe(status => {
- if (status === Toaster.Status.confirm) {
- this.store.dispatch(new DeleteBook(id));
- }
- });
-}
-```
-
-The `delete` method shows a confirmation popup and subscribes for the user response. `DeleteBook` action dispatched only if user clicks to the `Yes` button. The confirmation popup looks like below:
-
-
-
-### Next Part
-
-See the [next part](Part-III.md) of this tutorial.
+* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
+* [With Angular UI](../Part-1?UI=NG)
diff --git a/docs/en/Tutorials/Angular/Part-III.md b/docs/en/Tutorials/Angular/Part-III.md
index 6601bfb938..65a7dc5714 100644
--- a/docs/en/Tutorials/Angular/Part-III.md
+++ b/docs/en/Tutorials/Angular/Part-III.md
@@ -1,178 +1,6 @@
-## Angular Tutorial - Part III
+# Tutorials
-### About this Tutorial
+## Application Development
-This is the third part of the Angular tutorial series. See all parts:
-
-- [Part I: Create the project and a book list page](Part-I.md)
-- [Part II: Create, Update and Delete books](Part-II.md)
-- **Part III: Integration Tests (this tutorial)**
-
-This part covers the **server side** tests. You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb).
-
-### Test Projects in the Solution
-
-There are multiple test projects in the solution:
-
-
-
-Each project is used to test the related application project. Test projects use the following libraries for testing:
-
-* [xunit](https://xunit.github.io/) as the main test framework.
-* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as an assertion library.
-* [NSubstitute](http://nsubstitute.github.io/) as a mocking library.
-
-### Adding Test Data
-
-Startup template contains the `BookStoreTestDataSeedContributor` class in the `Acme.BookStore.TestBase` project that creates some data to run tests on.
-
-Change the `BookStoreTestDataSeedContributor` class as show below:
-
-````C#
-using System;
-using System.Threading.Tasks;
-using Volo.Abp.Data;
-using Volo.Abp.DependencyInjection;
-using Volo.Abp.Domain.Repositories;
-using Volo.Abp.Guids;
-
-namespace Acme.BookStore
-{
- public class BookStoreTestDataSeedContributor
- : IDataSeedContributor, ITransientDependency
- {
- private readonly IRepository _bookRepository;
- private readonly IGuidGenerator _guidGenerator;
-
- public BookStoreTestDataSeedContributor(
- IRepository bookRepository,
- IGuidGenerator guidGenerator)
- {
- _bookRepository = bookRepository;
- _guidGenerator = guidGenerator;
- }
-
- public async Task SeedAsync(DataSeedContext context)
- {
- await _bookRepository.InsertAsync(
- new Book
- {
- Id = _guidGenerator.Create(),
- Name = "Test book 1",
- Type = BookType.Fantastic,
- PublishDate = new DateTime(2015, 05, 24),
- Price = 21
- }
- );
-
- await _bookRepository.InsertAsync(
- new Book
- {
- Id = _guidGenerator.Create(),
- Name = "Test book 2",
- Type = BookType.Science,
- PublishDate = new DateTime(2014, 02, 11),
- Price = 15
- }
- );
- }
- }
-}
-````
-
-* Injected `IRepository` and used it in the `SeedAsync` to create two book entities as the test data.
-* Used `IGuidGenerator` service to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases (see the [Guid generation document](../../Guid-Generation.md) for more).
-
-### Testing the BookAppService
-
-Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project:
-
-````C#
-using System.Threading.Tasks;
-using Shouldly;
-using Volo.Abp.Application.Dtos;
-using Xunit;
-
-namespace Acme.BookStore
-{
- public class BookAppService_Tests : BookStoreApplicationTestBase
- {
- private readonly IBookAppService _bookAppService;
-
- public BookAppService_Tests()
- {
- _bookAppService = 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 == "Test book 1");
- }
- }
-}
-````
-
-* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of users.
-
-Add a new test that creates a valid new book:
-
-````C#
-[Fact]
-public async Task Should_Create_A_Valid_Book()
-{
- //Act
- var result = await _bookAppService.CreateAsync(
- new CreateUpdateBookDto
- {
- Name = "New test book 42",
- Price = 10,
- PublishDate = DateTime.Now,
- Type = BookType.ScienceFiction
- }
- );
-
- //Assert
- result.Id.ShouldNotBe(Guid.Empty);
- result.Name.ShouldBe("New test book 42");
-}
-````
-
-Add a new test that tries to create an invalid book and fails:
-
-````C#
-[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(mem => mem == "Name"));
-}
-````
-
-* Since the `Name` is empty, ABP throws an `AbpValidationException`.
-
-Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests:
-
-
-
-Congratulations, green icons show that tests have been successfully passed!
\ No newline at end of file
+* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
+* [With Angular UI](../Part-1?UI=NG)
diff --git a/docs/en/Tutorials/Angular/images/bookstore-actions-buttons.png b/docs/en/Tutorials/Angular/images/bookstore-actions-buttons.png
deleted file mode 100644
index aecf31c1ad..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-actions-buttons.png and /dev/null differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-angular-file-tree.png b/docs/en/Tutorials/Angular/images/bookstore-angular-file-tree.png
deleted file mode 100644
index be05ad3e4a..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-angular-file-tree.png and /dev/null differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-backend-solution-v2.png b/docs/en/Tutorials/Angular/images/bookstore-backend-solution-v2.png
deleted file mode 100644
index 79bcecb561..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-backend-solution-v2.png and /dev/null differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-book-list.png b/docs/en/Tutorials/Angular/images/bookstore-book-list.png
deleted file mode 100644
index 3f9717df6e..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-book-list.png and /dev/null differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-creating-book-list-terminal.png b/docs/en/Tutorials/Angular/images/bookstore-creating-book-list-terminal.png
deleted file mode 100644
index 9f01e94121..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-creating-book-list-terminal.png and /dev/null differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-creating-books-module-terminal.png b/docs/en/Tutorials/Angular/images/bookstore-creating-books-module-terminal.png
deleted file mode 100644
index c74b37b1b5..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-creating-books-module-terminal.png and /dev/null differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-edit-modal.png b/docs/en/Tutorials/Angular/images/bookstore-edit-modal.png
deleted file mode 100644
index c911403792..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-edit-modal.png and /dev/null differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-final-actions-dropdown.png b/docs/en/Tutorials/Angular/images/bookstore-final-actions-dropdown.png
deleted file mode 100644
index 6b0be415c4..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-final-actions-dropdown.png and /dev/null differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-initial-book-list-page.png b/docs/en/Tutorials/Angular/images/bookstore-initial-book-list-page.png
deleted file mode 100644
index 0b345ac61d..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-initial-book-list-page.png and /dev/null differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-initial-books-page-with-layout.png b/docs/en/Tutorials/Angular/images/bookstore-initial-books-page-with-layout.png
deleted file mode 100644
index 484837f78a..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-initial-books-page-with-layout.png and /dev/null differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-initial-books-page.png b/docs/en/Tutorials/Angular/images/bookstore-initial-books-page.png
deleted file mode 100644
index 4af86c50fa..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-initial-books-page.png and /dev/null differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-service-terminal-output.png b/docs/en/Tutorials/Angular/images/bookstore-service-terminal-output.png
deleted file mode 100644
index 69aaccba31..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-service-terminal-output.png and /dev/null differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-swagger-api.png b/docs/en/Tutorials/Angular/images/bookstore-swagger-api.png
deleted file mode 100644
index 437c772503..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-swagger-api.png and /dev/null differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-test-explorer.png b/docs/en/Tutorials/Angular/images/bookstore-test-explorer.png
deleted file mode 100644
index 06e9e7d331..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-test-explorer.png and /dev/null differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-test-projects-v3.png b/docs/en/Tutorials/Angular/images/bookstore-test-projects-v3.png
deleted file mode 100644
index 32ea91b325..0000000000
Binary files a/docs/en/Tutorials/Angular/images/bookstore-test-projects-v3.png and /dev/null differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md
index a1fbd1de54..65a7dc5714 100644
--- a/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md
+++ b/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md
@@ -1,476 +1,6 @@
-## ASP.NET Core MVC Tutorial - Part I
+# Tutorials
-### About this Tutorial
+## Application Development
-In this tutorial series, you will build an application that is used to manage a list of books & their authors. **Entity Framework Core** (EF Core) will be used as the ORM provider as it is the default database provider.
-
-This is the first part of the ASP.NET Core MVC tutorial series. See all parts:
-
-- **Part I: Create the project and a book list page (this tutorial)**
-- [Part II: Create, Update and Delete books](Part-II.md)
-- [Part III: Integration Tests](Part-III.md)
-
-You can access to the **source code** of the application from [the GitHub repository](https://github.com/abpframework/abp/tree/master/samples/BookStore).
-
-> You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial.
-
-### Creating the Project
-
-Create a new project named `Acme.BookStore`, create the database and run the application by following the [Getting Started document](../../Getting-Started-AspNetCore-MVC-Template.md).
-
-### Solution Structure
-
-This is how the layered solution structure looks after it's created:
-
-
-
-> You can see the [Application template document](../../Startup-Templates/Application.md) to understand the solution structure in details. However, you will understand the basics with this tutorial.
-
-### Create the Book Entity
-
-Domain layer in the startup template is separated into two projects:
-
-- `Acme.BookStore.Domain` contains your [entities](../../Entities.md), [domain services](../../Domain-Services.md) and other core domain objects.
-- `Acme.BookStore.Domain.Shared` contains constants, enums or other domain related objects those can be shared with clients.
-
-Define [entities](../../Entities.md) in the **domain layer** (`Acme.BookStore.Domain` project) of the solution. The main entity of the application is the `Book`. Create a class, named `Book`, in the `Acme.BookStore.Domain` project as shown below:
-
-````C#
-using System;
-using Volo.Abp.Domain.Entities.Auditing;
-
-namespace Acme.BookStore
-{
- public class Book : AuditedAggregateRoot
- {
- public string Name { get; set; }
-
- public BookType Type { get; set; }
-
- public DateTime PublishDate { get; set; }
-
- public float Price { get; set; }
-
- protected Book()
- {
-
- }
-
- public Book(Guid id, string name, BookType type, DateTime publishDate, float price)
- :base(id)
- {
- Name = name;
- Type = type;
- PublishDate = publishDate;
- Price = price;
- }
- }
-}
-````
-
-* ABP has two fundamental base classes for entities: `AggregateRoot` and `Entity`. **Aggregate Root** is one of the **Domain Driven Design (DDD)** concepts. See [entity document](../../Entities.md) for details and best practices.
-* `Book` entity inherits `AuditedAggregateRoot` which adds some auditing properties (`CreationTime`, `CreatorId`, `LastModificationTime`... etc.) on top of the `AggregateRoot` class.
-* `Guid` is the **primary key type** of the `Book` entity.
-
-#### BookType Enum
-
-Define the `BookType` enum in the `Acme.BookStore.Domain.Shared` project:
-
-````C#
-namespace Acme.BookStore
-{
- public enum BookType
- {
- Undefined,
- Adventure,
- Biography,
- Dystopia,
- Fantastic,
- Horror,
- Science,
- ScienceFiction,
- Poetry
- }
-}
-````
-
-#### Add Book Entity to Your DbContext
-
-EF Core requires you to relate entities with your DbContext. The easiest way to do this is to add a `DbSet` property to the `BookStoreDbContext` class in the `Acme.BookStore.EntityFrameworkCore` project, as shown below:
-
-````C#
- public class BookStoreDbContext : AbpDbContext
- {
- public DbSet Books { get; set; }
- ...
- }
-````
-
-#### Configure Your Book Entity
-
-Open `BookStoreDbContextModelCreatingExtensions.cs` file in the `Acme.BookStore.EntityFrameworkCore` project and add following code to the end of the `ConfigureBookStore` method to configure the Book entity:
-
-````C#
-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 New Migration & Update the Database
-
-The Startup template uses [EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to create and maintain the database schema. Open the **Package Manager Console (PMC)** (under the *Tools/Nuget Package Manager* menu), select the `Acme.BookStore.EntityFrameworkCore.DbMigrations` as the **default project** and execute the following command:
-
-
-
-This will create a new migration class inside the `Migrations` folder. Then execute the `Update-Database` command to update the database schema:
-
-````
-PM> Update-Database
-````
-
-#### Add Sample Data
-
-`Update-Database` command created the `AppBooks` table in the database. Open your database and enter a few sample rows, so you can show them on the page:
-
-
-
-### Create the Application Service
-
-The next step is to create an [application service](../../Application-Services.md) to manage (create, list, update, delete...) the books. Application layer in the startup template is separated into two projects:
-
-* `Acme.BookStore.Application.Contracts` mainly contains your DTOs and application service interfaces.
-* `Acme.BookStore.Application` contains the implementations of your application services.
-
-#### BookDto
-
-Create a DTO class named `BookDto` into the `Acme.BookStore.Application.Contracts` project:
-
-````C#
-using System;
-using Volo.Abp.Application.Dtos;
-
-namespace Acme.BookStore
-{
- public class BookDto : AuditedEntityDto
- {
- public string Name { get; set; }
-
- public BookType Type { get; set; }
-
- public DateTime PublishDate { get; set; }
-
- public float Price { get; set; }
- }
-}
-````
-
-* **DTO** classes are used to **transfer data** between the *presentation layer* and the *application layer*. See the [Data Transfer Objects document](../../Data-Transfer-Objects.md) for more details.
-* `BookDto` is used to transfer book data to the presentation layer in order to show the book information on the UI.
-* `BookDto` is derived from the `AuditedEntityDto` which has audit properties just like the `Book` class defined above.
-
-It will be needed to convert `Book` entities to `BookDto` objects while returning books to the presentation layer. [AutoMapper](https://automapper.org) library can automate this conversion when you define the proper mapping. Startup template comes with AutoMapper configured, so you can just define the mapping in the `BookStoreApplicationAutoMapperProfile` class in the `Acme.BookStore.Application` project:
-
-````csharp
-using AutoMapper;
-
-namespace Acme.BookStore
-{
- public class BookStoreApplicationAutoMapperProfile : Profile
- {
- public BookStoreApplicationAutoMapperProfile()
- {
- CreateMap();
- }
- }
-}
-````
-
-#### CreateUpdateBookDto
-
-Create a DTO class named `CreateUpdateBookDto` into the `Acme.BookStore.Application.Contracts` project:
-
-````c#
-using System;
-using System.ComponentModel.DataAnnotations;
-
-namespace Acme.BookStore
-{
- public class CreateUpdateBookDto
- {
- [Required]
- [StringLength(128)]
- public string Name { get; set; }
-
- [Required]
- public BookType Type { get; set; } = BookType.Undefined;
-
- [Required]
- public DateTime PublishDate { get; set; }
-
- [Required]
- public float Price { get; set; }
- }
-}
-````
-
-* This DTO class is used to get book information from the user interface while creating or updating a book.
-* It defines data annotation attributes (like `[Required]`) to define validations for the properties. DTOs are [automatically validated](../../Validation.md) by the ABP framework.
-
-Next, add a mapping in `BookStoreApplicationAutoMapperProfile` from the `CreateUpdateBookDto` object to the `Book` entity:
-
-````csharp
-CreateMap();
-````
-
-#### IBookAppService
-
-Define an interface named `IBookAppService` in the `Acme.BookStore.Application.Contracts` project:
-
-````C#
-using System;
-using Volo.Abp.Application.Dtos;
-using Volo.Abp.Application.Services;
-
-namespace Acme.BookStore
-{
- public interface IBookAppService :
- ICrudAppService< //Defines CRUD methods
- BookDto, //Used to show books
- Guid, //Primary key of the book entity
- PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books
- CreateUpdateBookDto, //Used to create a new book
- CreateUpdateBookDto> //Used to update a book
- {
-
- }
-}
-````
-
-* Defining interfaces for application services is not required by the framework. However, it's suggested as a best practice.
-* `ICrudAppService` defines common **CRUD** methods: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync` and `DeleteAsync`. It's not required to extend it. Instead, you could inherit from the empty `IApplicationService` interface and define your own methods manually.
-* There are some variations of the `ICrudAppService` where you can use separated DTOs for each method.
-
-#### BookAppService
-
-Implement the `IBookAppService` as named `BookAppService` in the `Acme.BookStore.Application` project:
-
-````C#
-using System;
-using Volo.Abp.Application.Dtos;
-using Volo.Abp.Application.Services;
-using Volo.Abp.Domain.Repositories;
-
-namespace Acme.BookStore
-{
- public class BookAppService :
- CrudAppService,
- IBookAppService
- {
- public BookAppService(IRepository repository)
- : base(repository)
- {
-
- }
- }
-}
-````
-
-* `BookAppService` is derived from `CrudAppService<...>` which implements all the CRUD methods defined above.
-* `BookAppService` injects `IRepository` which is the default repository for the `Book` entity. ABP automatically creates default repositories for each aggregate root (or entity). See the [repository document](../../Repositories.md).
-* `BookAppService` uses `IObjectMapper` to convert `Book` objects to `BookDto` objects and `CreateUpdateBookDto` objects to `Book` objects. The Startup template uses the [AutoMapper](http://automapper.org/) library as the object mapping provider. You defined the mappings before, so it will work as expected.
-
-### Auto API Controllers
-
-You normally create **Controllers** to expose application services as **HTTP API** endpoints. Thus allowing browser or 3rd-party clients to call them via AJAX. ABP can [**automagically**](../../AspNetCore/Auto-API-Controllers.md) configures your application services as MVC API Controllers by convention.
-
-#### Swagger UI
-
-The startup template is configured to run the [swagger UI](https://swagger.io/tools/swagger-ui/) using the [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) library. Run the application and enter `https://localhost:XXXX/swagger/` (replace XXXX by your own port) as URL on your browser.
-
-You will see some built-in service endpoints as well as the `Book` service and its REST-style endpoints:
-
-
-
-Swagger has a nice UI to test APIs. You can try to execute the `[GET] /api/app/book` API to get a list of books.
-
-### Dynamic JavaScript Proxies
-
-It's common to call 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** for all API endpoints. So, you can use any **endpoint** just like calling a **JavaScript function**.
-
-#### Testing in the Browser Developer Console
-
-You can easily test the JavaScript proxies using your favorite browser's **Developer Console** now. Run the application, open your browser's **developer tools** (shortcut: F12), switch to the **Console** tab, type the following code and press enter:
-
-````js
-acme.bookStore.book.getList({}).done(function (result) { console.log(result); });
-````
-
-* `acme.bookStore` is the namespace of the `BookAppService` converted to [camelCase](https://en.wikipedia.org/wiki/Camel_case).
-* `book` is the conventional name for the `BookAppService` (removed AppService postfix and converted to camelCase).
-* `getList` is the conventional name for the `GetListAsync` method defined in the `AsyncCrudAppService` base class (removed Async postfix and converted to camelCase).
-* `{}` argument is used to send an empty object to the `GetListAsync` method which normally expects an object of type `PagedAndSortedResultRequestDto` that is used to send paging and sorting options to the server (all properties are optional, so you can send an empty object).
-* `getList` function returns a `promise`. So, you can pass a callback to the `done` (or `then`) function to get the result from the server.
-
-Running this code produces the following output:
-
-
-
-You can see the **book list** returned from the server. You can also check the **network** tab of the developer tools to see the client to server communication:
-
-
-
-Let's **create a new book** using the `create` function:
-
-````js
-acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-24', price: 21.5 }).done(function (result) { console.log('successfully created the book with id: ' + result.id); });
-````
-
-You should see a message in the console something like that:
-
-````
-successfully created the book with id: f3f03580-c1aa-d6a9-072d-39e75c69f5c7
-````
-
-Check the `Books` table in the database to see the new book row. You can try `get`, `update` and `delete` functions yourself.
-
-### Create the Books Page
-
-It's time to create something visible and usable! Instead of classic MVC, we will use the new [Razor Pages UI](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start) approach which is recommended by Microsoft.
-
-Create a new `Books` folder under the `Pages` folder of the `Acme.BookStore.Web` project and add a new Razor Page named `Index.cshtml`:
-
-
-
-Open the `Index.cshtml` and change the content as shown below:
-
-````html
-@page
-@using Acme.BookStore.Web.Pages.Books
-@inherits Acme.BookStore.Web.Pages.BookStorePage
-@model IndexModel
-
-
Books
-````
-
-* This code changes the default inheritance of the Razor View Page Model so it **inherits** from the `BookStorePage` class (instead of `PageModel`). The `BookStorePage` class which comes with the startup template and provides some shared properties/methods used by all pages.
-* Ensure that the `IndexModel` (*Index.cshtml.cs)* has the `Acme.BookStore.Web.Pages.Books` namespace, or update it in the `Index.cshtml`.
-
-#### Add Books Page to the Main Menu
-
-Open the `BookStoreMenuContributor` class in the `Menus` folder and add the following code to the end of the `ConfigureMainMenuAsync` method:
-
-````c#
-context.Menu.AddItem(
- new ApplicationMenuItem("BooksStore", l["Menu:BookStore"])
- .AddItem(new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books"))
-);
-````
-
-#### Localizing the Menu Items
-
-Localization texts are located under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project:
-
-
-
-Open the `en.json` file and add localization texts for `Menu:BookStore` and `Menu:Books` keys to the end of the file:
-
-````json
-{
- "culture": "en",
- "texts": {
- "Menu:BookStore": "Book Store",
- "Menu:Books": "Books"
- }
-}
-````
-
-* ABP's localization system is built on [ASP.NET Core's standard localization](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) system and extends it in many ways. See the [localization document](../../Localization.md) for details.
-* Localization key names are arbitrary. You can set any name. We prefer to add `Menu:` prefix for menu items to distinguish from other texts. If a text is not defined in the localization file, it **fallbacks** to the localization key (ASP.NET Core's standard behavior).
-
-Run the application and see the new menu item has been added to the top bar:
-
-
-
-When you click to the Books menu item, you are redirected to the new Books page.
-
-#### Book List
-
-We will use the [Datatables.net](https://datatables.net/) JQuery plugin to show list of tables on the page. Datatables can completely work via AJAX, it is fast and provides a good user experience. Datatables plugin is configured in the startup template, so you can directly use it in any page without including any style or script file to your page.
-
-##### Index.cshtml
-
-Change the `Pages/Books/Index.cshtml` as following:
-
-````html
-@page
-@inherits Acme.BookStore.Web.Pages.BookStorePage
-@model Acme.BookStore.Web.Pages.Books.IndexModel
-@section scripts
-{
-
-}
-
-
-
@L["Books"]
-
-
-
-
-
-
@L["Name"]
-
@L["Type"]
-
@L["PublishDate"]
-
@L["Price"]
-
@L["CreationTime"]
-
-
-
-
-
-````
-
-* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro) is used to add external **scripts** to the page. It has many additional features compared to standard `script` tag. It handles **minification** and **versioning** for example. See the [bundling & minification document](../../AspNetCore/Bundling-Minification.md) for details.
-* `abp-card` and `abp-table` are **tag helpers** for Twitter Bootstrap's [card component](http://getbootstrap.com/docs/4.1/components/card/). There are many tag helpers in ABP to easily use most of the [bootstrap](https://getbootstrap.com/) components. You can also use regular HTML tags instead of these tag helpers, but using tag helpers reduces HTML code and prevents errors by help of the intellisense and compile time type checking. See the [tag helpers document](../../AspNetCore/Tag-Helpers/Index.md).
-* You can **localize** the column names in the localization file as you did for the menu items above.
-
-##### Add a Script File
-
-Create `index.js` JavaScript file under the `Pages/Books/` folder:
-
-
-
-`index.js` content is shown below:
-
-````js
-$(function () {
- var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({
- ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
- columnDefs: [
- { data: "name" },
- { data: "type" },
- { data: "publishDate" },
- { data: "price" },
- { data: "creationTime" }
- ]
- }));
-});
-````
-
-* `abp.libs.datatables.createAjax` is a helper function to adapt ABP's dynamic JavaScript API proxies to Datatable's format.
-* `abp.libs.datatables.normalizeConfiguration` is another helper function. There's no requirement to use it, but it simplifies the datatables configuration by providing conventional values for missing options.
-* `acme.bookStore.book.getList` is the function to get list of books (you have seen it before).
-* See [Datatable's documentation](https://datatables.net/manual/) for more configuration options.
-
-The final UI is shown below:
-
-
-
-### Next Part
-
-See the [next part](Part-II.md) of this tutorial.
+* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
+* [With Angular UI](../Part-1?UI=NG)
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md
index 505de55adc..65a7dc5714 100644
--- a/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md
+++ b/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md
@@ -1,432 +1,6 @@
-## ASP.NET Core MVC Tutorial - Part II
+# Tutorials
-### About this Tutorial
+## Application Development
-This is the second part of the ASP.NET Core MVC tutorial series. See all parts:
-
-* [Part I: Create the project and a book list page](Part-I.md)
-* **Part II: Create, Update and Delete books (this tutorial)**
-* [Part III: Integration Tests](Part-III.md)
-
-You can access to the **source code** of the application from [the GitHub repository](https://github.com/volosoft/abp/tree/master/samples/BookStore).
-
-> You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial.
-
-### Creating a New Book
-
-In this section, you will learn how to create a new modal dialog form to create a new book. The result dialog will be like that:
-
-
-
-#### Create the Modal Form
-
-Create a new razor page, named `CreateModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project:
-
-
-
-##### CreateModal.cshtml.cs
-
-Open the `CreateModal.cshtml.cs` file (`CreateModalModel` class) and replace with the following code:
-
-````C#
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Acme.BookStore.Web.Pages.Books
-{
- public class CreateModalModel : BookStorePageModel
- {
- [BindProperty]
- public CreateUpdateBookDto Book { get; set; }
-
- private readonly IBookAppService _bookAppService;
-
- public CreateModalModel(IBookAppService bookAppService)
- {
- _bookAppService = bookAppService;
- }
-
- public async Task OnPostAsync()
- {
- await _bookAppService.CreateAsync(Book);
- return NoContent();
- }
- }
-}
-````
-
-* This class is derived from the `BookStorePageModel` instead of standard `PageModel`. `BookStorePageModel` inherits the `PageModel` and adds some common properties/methods those can be used by your page model classes.
-* `[BindProperty]` attribute on the `Book` property binds post request data to this property.
-* This class simply injects the `IBookAppService` in its constructor and calls the `CreateAsync` method in the `OnPostAsync` handler.
-
-##### CreateModal.cshtml
-
-Open the `CreateModal.cshtml` file and paste the code below:
-
-````html
-@page
-@inherits Acme.BookStore.Web.Pages.BookStorePage
-@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
-@model Acme.BookStore.Web.Pages.Books.CreateModalModel
-@{
- Layout = null;
-}
-
-
-
-
-
-
-
-
-
-````
-
-* This modal uses `abp-dynamic-form` tag helper to automatically create the form from the `CreateBookViewModel` class.
- * `abp-model` attribute indicates the model object, the `Book` property in this case.
- * `data-ajaxForm` attribute makes the form submitting via AJAX, instead of a classic page post.
- * `abp-form-content` tag helper is a placeholder to render the form controls (this is optional and needed only if you added some other content in the `abp-dynamic-form` tag, just like in this page).
-
-#### Add the "New book" Button
-
-Open the `Pages/Books/Index.cshtml` and change the `abp-card-header` tag as shown below:
-
-````html
-
-
-
-
@L["Books"]
-
-
-
-
-
-
-````
-
-Just added a **New book** button to the **top right** of the table:
-
-
-
-Open the `pages/books/index.js` and add the following code just after the datatable configuration:
-
-````js
-var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
-
-createModal.onResult(function () {
- dataTable.ajax.reload();
-});
-
-$('#NewBookButton').click(function (e) {
- e.preventDefault();
- createModal.open();
-});
-````
-
-* `abp.ModalManager` is a helper class to open and manage modals in the client side. It internally uses Twitter Bootstrap's standard modal, but abstracts many details by providing a simple API.
-
-Now, you can **run the application** and add new books using the new modal form.
-
-### Updating An Existing Book
-
-Create a new razor page, named `EditModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project:
-
-
-
-#### EditModal.cshtml.cs
-
-Open the `EditModal.cshtml.cs` file (`EditModalModel` class) and replace with the following code:
-
-````csharp
-using System;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Acme.BookStore.Web.Pages.Books
-{
- public class EditModalModel : BookStorePageModel
- {
- [HiddenInput]
- [BindProperty(SupportsGet = true)]
- public Guid Id { get; set; }
-
- [BindProperty]
- public CreateUpdateBookDto 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();
- }
- }
-}
-````
-
-* `[HiddenInput]` and `[BindProperty]` are standard ASP.NET Core MVC attributes. Used `SupportsGet` to be able to get Id value from query string parameter of the request.
-* Mapped `BookDto` (received from the `BookAppService.GetAsync`) to `CreateUpdateBookDto` in the `GetAsync` method.
-* The `OnPostAsync` simply uses `BookAppService.UpdateAsync` to update the entity.
-
-#### BookDto to CreateUpdateBookDto Mapping
-
-In order to perform `BookDto` to `CreateUpdateBookDto` object mapping, open the `BookStoreWebAutoMapperProfile.cs` in the `Acme.BookStore.Web` project and change it as shown below:
-
-````csharp
-using AutoMapper;
-
-namespace Acme.BookStore.Web
-{
- public class BookStoreWebAutoMapperProfile : Profile
- {
- public BookStoreWebAutoMapperProfile()
- {
- CreateMap();
- }
- }
-}
-````
-
-* Just added `CreateMap();` as the mapping definition.
-
-#### EditModal.cshtml
-
-Replace `EditModal.cshtml` content with the following content:
-
-````html
-@page
-@inherits Acme.BookStore.Web.Pages.BookStorePage
-@using Acme.BookStore.Web.Pages.Books
-@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
-@model EditModalModel
-@{
- Layout = null;
-}
-
-
-
-
-
-
-
-
-
-
-````
-
-This page is very similar to the `CreateModal.cshtml` except;
-
-* It includes an `abp-input` for the `Id` property to store id of the editing book (which is a hidden input).
-* It uses `Books/EditModal` as the post URL and *Update* text as the modal header.
-
-#### Add "Actions" Dropdown to the Table
-
-We will add a dropdown button ("Actions") for each row of the table. The final UI looks like this:
-
-
-
-Open the `Pages/Books/Index.cshtml` page and change the table section as shown below:
-
-````html
-
-
-
-
@L["Actions"]
-
@L["Name"]
-
@L["Type"]
-
@L["PublishDate"]
-
@L["Price"]
-
@L["CreationTime"]
-
-
-
-````
-
-* Just added a new `th` tag for the "Actions".
-
-Open the `pages/books/index.js` and replace the content as below:
-
-````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({
- processing: true,
- serverSide: true,
- paging: true,
- searching: false,
- autoWidth: false,
- scrollCollapse: true,
- order: [[1, "asc"]],
- ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
- columnDefs: [
- {
- rowAction: {
- items:
- [
- {
- text: l('Edit'),
- action: function (data) {
- editModal.open({ id: data.record.id });
- }
- }
- ]
- }
- },
- { data: "name" },
- { data: "type" },
- { data: "publishDate" },
- { data: "price" },
- { data: "creationTime" }
- ]
- }));
-
- createModal.onResult(function () {
- dataTable.ajax.reload();
- });
-
- editModal.onResult(function () {
- dataTable.ajax.reload();
- });
-
- $('#NewBookButton').click(function (e) {
- e.preventDefault();
- createModal.open();
- });
-});
-````
-
-* Used `abp.localization.getResource('BookStore')` to be able to use the same localization texts defined on the server side.
-* Added a new `ModalManager` named `createModal` to open the create modal dialog.
-* Added a new `ModalManager` named `editModal` to open the edit modal dialog.
-* Added a new column at the beginning of the `columnDefs` section. This column is used for the "Actions" dropdown button.
-* "New Book" action simply calls `createModal.open` to open the create dialog.
-* "Edit" action simply calls `editModal.open` to open the edit dialog.
-`
-You can run the application and edit any book by selecting the edit action.
-
-### Deleting an Existing Book
-
-Open the `pages/books/index.js` and add a new item to the `rowAction` `items`:
-
-````js
-{
- text: l('Delete'),
- confirmMessage: function (data) {
- return l('BookDeletionConfirmationMessage', data.record.name);
- },
- action: function (data) {
- acme.bookStore.book
- .delete(data.record.id)
- .then(function() {
- abp.notify.info(l('SuccessfullyDeleted'));
- dataTable.ajax.reload();
- });
- }
-}
-````
-
-* `confirmMessage` option is used to ask a confirmation question before executing the `action`.
-* Used `acme.bookStore.book.delete` javascript proxy function to perform an AJAX request to delete a book.
-* `abp.notify.info` is used to show a toastr notification just after the deletion.
-
-The final `index.js` content is shown below:
-
-````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({
- processing: true,
- serverSide: true,
- paging: true,
- searching: false,
- autoWidth: false,
- scrollCollapse: true,
- order: [[1, "asc"]],
- ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
- columnDefs: [
- {
- rowAction: {
- items:
- [
- {
- text: l('Edit'),
- action: function (data) {
- editModal.open({ id: data.record.id });
- }
- },
- {
- text: l('Delete'),
- confirmMessage: function (data) {
- return l('BookDeletionConfirmationMessage', data.record.name);
- },
- action: function (data) {
- acme.bookStore.book
- .delete(data.record.id)
- .then(function() {
- abp.notify.info(l('SuccessfullyDeleted'));
- dataTable.ajax.reload();
- });
- }
- }
- ]
- }
- },
- { data: "name" },
- { data: "type" },
- { data: "publishDate" },
- { data: "price" },
- { data: "creationTime" }
- ]
- }));
-
- createModal.onResult(function () {
- dataTable.ajax.reload();
- });
-
- editModal.onResult(function () {
- dataTable.ajax.reload();
- });
-
- $('#NewBookButton').click(function (e) {
- e.preventDefault();
- createModal.open();
- });
-});
-````
-
-Open the `en.json` in the `Acme.BookStore.Domain.Shared` project and add the following line:
-
-````json
-"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?",
-"SuccessfullyDeleted": "Successfully deleted"
-````
-
-Run the application and try to delete a book.
-
-### Next Part
-
-See the [next part](Part-III.md) of this tutorial.
+* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
+* [With Angular UI](../Part-1?UI=NG)
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md
index f207d9df0c..65a7dc5714 100644
--- a/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md
+++ b/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md
@@ -1,166 +1,6 @@
-## ASP.NET Core MVC Tutorial - Part III
+# Tutorials
-### About this Tutorial
+## Application Development
-This is the third part of the ASP.NET Core MVC tutorial series. See all parts:
-
-- [Part I: Create the project and a book list page](Part-I.md)
-- [Part II: Create, Update and Delete books](Part-II.md)
-- **Part III: Integration Tests (this tutorial)**
-
-You can access to the **source code** of the application from [the GitHub repository](https://github.com/volosoft/abp/tree/master/samples/BookStore).
-
-> You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial.
-
-### Test Projects in the Solution
-
-There are multiple test projects in the solution:
-
-
-
-Each project is used to test the related application project. Test projects use the following libraries for testing:
-
-* [xunit](https://xunit.github.io/) as the main test framework.
-* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as an assertion library.
-* [NSubstitute](http://nsubstitute.github.io/) as a mocking library.
-
-### Adding Test Data
-
-Startup template contains the `BookStoreTestDataSeedContributor` class in the `Acme.BookStore.TestBase` project that creates some data to run tests on.
-
-Change the `BookStoreTestDataSeedContributor` class as show below:
-
-````C#
-using System;
-using System.Threading.Tasks;
-using Volo.Abp.Data;
-using Volo.Abp.DependencyInjection;
-using Volo.Abp.Domain.Repositories;
-using Volo.Abp.Guids;
-
-namespace Acme.BookStore
-{
- public class BookStoreTestDataSeedContributor
- : IDataSeedContributor, ITransientDependency
- {
- private readonly IRepository _bookRepository;
- private readonly IGuidGenerator _guidGenerator;
-
- public BookStoreTestDataSeedContributor(
- IRepository bookRepository,
- IGuidGenerator guidGenerator)
- {
- _bookRepository = bookRepository;
- _guidGenerator = guidGenerator;
- }
-
- public async Task SeedAsync(DataSeedContext context)
- {
- await _bookRepository.InsertAsync(
- new Book(_guidGenerator.Create(), "Test book 1", BookType.Fantastic, new DateTime(2015, 05, 24), 21)
- );
-
- await _bookRepository.InsertAsync(
- new Book(_guidGenerator.Create(), "Test book 2", BookType.Science, new DateTime(2014, 02, 11), 15)
- );
- }
- }
-}
-````
-
-* Injected `IRepository` and used it in the `SeedAsync` to create two book entities as the test data.
-* Used `IGuidGenerator` service to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases (see the [Guid generation document](../../Guid-Generation.md) for more).
-
-### Testing the BookAppService
-
-Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project:
-
-````C#
-using System.Threading.Tasks;
-using Shouldly;
-using Volo.Abp.Application.Dtos;
-using Xunit;
-
-namespace Acme.BookStore
-{
- public class BookAppService_Tests : BookStoreApplicationTestBase
- {
- private readonly IBookAppService _bookAppService;
-
- public BookAppService_Tests()
- {
- _bookAppService = 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 == "Test book 1");
- }
- }
-}
-````
-
-* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of users.
-
-Add a new test that creates a valid new book:
-
-````C#
-[Fact]
-public async Task Should_Create_A_Valid_Book()
-{
- //Act
- var result = await _bookAppService.CreateAsync(
- new CreateUpdateBookDto
- {
- Name = "New test book 42",
- Price = 10,
- PublishDate = DateTime.Now,
- Type = BookType.ScienceFiction
- }
- );
-
- //Assert
- result.Id.ShouldNotBe(Guid.Empty);
- result.Name.ShouldBe("New test book 42");
-}
-````
-
-Add a new test that tries to create an invalid book and fails:
-
-````C#
-[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(mem => mem == "Name"));
-}
-````
-
-* Since the `Name` is empty, ABP throws an `AbpValidationException`.
-
-Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests:
-
-
-
-Congratulations, green icons show that tests have been successfully passed!
\ No newline at end of file
+* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC)
+* [With Angular UI](../Part-1?UI=NG)
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png
deleted file mode 100644
index a7d49a661b..0000000000
Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png and /dev/null differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png
deleted file mode 100644
index f531e6f457..0000000000
Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png and /dev/null differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png
deleted file mode 100644
index 7cc96c8c94..0000000000
Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png and /dev/null differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png
deleted file mode 100644
index 79314dd2dc..0000000000
Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png and /dev/null differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png
deleted file mode 100644
index dfd4b5d8aa..0000000000
Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png and /dev/null differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png
deleted file mode 100644
index edf2826361..0000000000
Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png and /dev/null differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png
deleted file mode 100644
index 437c772503..0000000000
Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png and /dev/null differ
diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md
new file mode 100644
index 0000000000..a6ceb26c83
--- /dev/null
+++ b/docs/en/Tutorials/Part-1.md
@@ -0,0 +1,1117 @@
+## ASP.NET Core {{UI_Value}} Tutorial - Part 1
+````json
+//[doc-params]
+{
+ "UI": ["MVC","NG"]
+}
+````
+{{
+if UI == "MVC"
+ DB="ef"
+ DB_Text="Entity Framework Core"
+ UI_Text="mvc"
+else if UI == "NG"
+ DB="mongodb"
+ DB_Text="MongoDB"
+ UI_Text="angular"
+else
+ DB ="?"
+ UI_Text="?"
+end
+}}
+
+### About this tutorial:
+
+In this tutorial series, you will build an ABP Commercial application named `Acme.BookStore`. In this sample project, we will manage a list of books and authors. **{{DB_Text}}** will be used as the ORM provider. And on the front-end side {{UI_Value}} and JavaScript will be used.
+
+The ASP.NET Core {{UI_Value}} tutorial series consists of 3 parts:
+
+- **Part-1: Creating the project and book list page (this tutorial)**
+- [Part-2: Creating, updating and deleting books](part-2.md)
+- [Part-3: Integration tests](part-3.md)
+
+*You can also check out [the video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by the community, based on this tutorial.*
+
+### Creating the project
+
+Create a new project named `Acme.BookStore` where `Acme` is the company name and `BookStore` is the project name. You can check out [creating a new project](../Getting-Started-{{if UI == 'NG'}}Angular{{else}}AspNetCore-MVC{{end}}-Template#creating-a-new-project) document to see how you can create a new project. We will create the project with ABP CLI. But first of all, we need to login to the ABP Platform to create a commercial project.
+
+#### Create the project
+
+By running the below command, it creates a new ABP Commercial project with the database provider `{{DB_Text}}` and UI option `MVC`. To see the other CLI options, check out [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) document.
+
+```bash
+abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}}
+```
+
+
+### Apply migrations
+
+After creating the project, you need to apply the initial migrations and create the database. To apply migrations, right click on the `Acme.BookStore.DbMigrator` and click **Debug** > **Start New Instance**. This will run the application and apply all migrations. You will see the below result when it successfully completes the process. The application database is ready!
+
+
+
+> Alternatively, you can run `Update-Database` command in the Visual Studio > Package Manager Console to apply migrations.
+
+#### Initial database tables
+
+
+
+### Run the application
+
+To run the project, right click to the {{if UI == "MVC"}} `Acme.BookStore.Web`{{end}} {{if UI == "NG"}} `Acme.BookStore.HttpApi.Host` {{end}} project and click **Set As StartUp Project**. And run the web project by pressing **CTRL+F5** (*without debugging and fast*) or press **F5** (*with debugging and slow*). {{if UI == "NG"}}You will see the Swagger UI for BookStore API.{{end}}
+
+Further information, see the [running the application section](../../Getting-Started-{{if UI == "NG"}}Angular{{else}}AspNetCore-MVC{{end}}-Template#running-the-application).Getting-Started-AspNetCore-MVC-Template#running-the-application
+
+
+
+{{if UI == "NG"}}
+
+To start Angular project, go to the `angular` folder, open a command line terminal, execute the `yarn` command:
+
+```bash
+yarn
+```
+
+Once all node modules are loaded, execute the `yarn start` command:
+
+```bash
+yarn start
+```
+
+The website will be accessible from the following default URL:
+
+http://localhost:4200/
+
+If you see the website's landing page successfully, you can exit Angular hosting by pressing `ctrl-c`. (We'll later start it again.)
+
+> Be aware that, Firefox does not use the Windows Certificate Store, so you'll need to add the self-signed developer certificate to Firefox manually. To do this, open Firefox and navigate to the below URL:
+>
+> https://localhost:44322/api/abp/application-configuration
+>
+> If you see the below screen, click the **Accept the Risk and Continue** button to bypass this warning.
+>
+> 
+
+{{end}}
+
+The default login credentials are;
+
+* **Username**: admin
+* **Password**: 1q2w3E*
+
+### Solution structure
+
+This is how the layered solution structure looks like:
+
+
+
+Check out the [solution structure](../startup-templates/application#solution-structure) section to understand the structure in details.
+
+### Create the book entity
+
+Domain layer in the startup template is separated into two projects:
+
+- `Acme.BookStore.Domain` contains your [entities](https://docs.abp.io/en/abp/latest/Entities), [domain services](https://docs.abp.io/en/abp/latest/Domain-Services) and other core domain objects.
+- `Acme.BookStore.Domain.Shared` contains `constants`, `enums` or other domain related objects those can be shared with clients.
+
+Define [entities](https://docs.abp.io/en/abp/latest/Entities) in the **domain layer** (`Acme.BookStore.Domain` project) of the solution. The main entity of the application is the `Book`. Create a class, named `Book`, in the `Acme.BookStore.Domain` project as shown below:
+
+````csharp
+using System;
+using Volo.Abp.Domain.Entities.Auditing;
+
+namespace Acme.BookStore
+{
+ public class Book : AuditedAggregateRoot
+ {
+ public string Name { get; set; }
+
+ public BookType Type { get; set; }
+
+ public DateTime PublishDate { get; set; }
+
+ public float Price { get; set; }
+
+ protected Book()
+ {
+
+ }
+
+ public Book(Guid id, string name, BookType type, DateTime publishDate, float price) :
+ base(id)
+ {
+ Name = name;
+ Type = type;
+ PublishDate = publishDate;
+ Price = price;
+ }
+ }
+}
+````
+
+* ABP has 2 fundamental base classes for entities: `AggregateRoot` and `Entity`. **Aggregate Root** is one of the **Domain Driven Design (DDD)** concepts. See [entity document](https://docs.abp.io/en/abp/latest/Entities) for details and best practices.
+* `Book` entity inherits `AuditedAggregateRoot` which adds some auditing properties (`CreationTime`, `CreatorId`, `LastModificationTime`... etc.) on top of the `AggregateRoot` class.
+* `Guid` is the **primary key type** of the `Book` entity.
+
+#### BookType enum
+
+Create the `BookType` enum in the `Acme.BookStore.Domain.Shared` project:
+
+````csharp
+namespace Acme.BookStore
+{
+ public enum BookType
+ {
+ Undefined,
+ Adventure,
+ Biography,
+ Dystopia,
+ Fantastic,
+ Horror,
+ Science,
+ ScienceFiction,
+ Poetry
+ }
+}
+````
+
+#### Add book entity to the DbContext
+
+{{if DB == "ef"}}
+
+EF Core requires to relate entities with your `DbContext`. The easiest way to do this is to add a `DbSet` property to the `BookStoreDbContext` class in the `Acme.BookStore.EntityFrameworkCore` project, as shown below:
+
+````csharp
+ public class BookStoreDbContext : AbpDbContext
+ {
+ public DbSet Users { get; set; }
+ public DbSet Books { get; set; } //<--added this line-->
+ //...
+ }
+````
+
+{{end}}
+
+{{if DB == "mongodb"}}
+
+Add a `IMongoCollection Books` property to the `BookStoreMongoDbContext` inside the `Acme.BookStore.MongoDB` project:
+
+```csharp
+public class BookStoreMongoDbContext : AbpMongoDbContext
+{
+ public IMongoCollection Users => Collection();
+ public IMongoCollection Books => Collection();//<--added this line-->
+ //...
+}
+```
+
+{{end}}
+
+{{if DB == "ef"}}
+
+#### Configure the book entity
+
+Open `BookStoreDbContextModelCreatingExtensions.cs` file in the `Acme.BookStore.EntityFrameworkCore` project and add following code to the end of the `ConfigureBookStore` method to configure the Book entity:
+
+````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 `using Volo.Abp.EntityFrameworkCore.Modeling;` statement to resolve `ConfigureByConvention` extension method.
+
+{{end}}
+
+{{if DB == "mongodb"}}
+
+#### Add seed (sample) data
+
+Adding sample data is optional, but it's good to have initial data in the database for the first run. ABP provides a [data seed system](https://docs.abp.io/en/abp/latest/Data-Seeding). Create a class deriving from the `IDataSeedContributor` in the `*.Domain` project:
+
+```csharp
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Data;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Domain.Repositories;
+using Volo.Abp.Guids;
+
+namespace Acme.BookStore
+{
+ public class BookStoreDataSeederContributor
+ : IDataSeedContributor, ITransientDependency
+ {
+ private readonly IRepository _bookRepository;
+ private readonly IGuidGenerator _guidGenerator;
+
+ public BookStoreDataSeederContributor(
+ IRepository bookRepository,
+ IGuidGenerator guidGenerator)
+ {
+ _bookRepository = bookRepository;
+ _guidGenerator = guidGenerator;
+ }
+
+ public async Task SeedAsync(DataSeedContext context)
+ {
+ if (await _bookRepository.GetCountAsync() > 0)
+ {
+ return;
+ }
+
+ await _bookRepository.InsertAsync(
+ new Book(
+ id: _guidGenerator.Create(),
+ name: "1984",
+ type: BookType.Dystopia,
+ publishDate: new DateTime(1949, 6, 8),
+ price: 19.84f
+ )
+ );
+
+ await _bookRepository.InsertAsync(
+ new Book(
+ id: _guidGenerator.Create(),
+ name: "The Hitchhiker's Guide to the Galaxy",
+ type: BookType.ScienceFiction,
+ publishDate: new DateTime(1995, 9, 27),
+ price: 42.0f
+ )
+ );
+ }
+ }
+}
+```
+
+{{end}}
+
+{{if DB == "ef"}}
+
+#### Add new migration & update the database
+
+The startup template uses [EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to create and maintain the database schema. Open the **Package Manager Console (PMC)** under the menu *Tools > NuGet Package Manager*.
+
+
+
+Select the `Acme.BookStore.EntityFrameworkCore.DbMigrations` as the **default project** and execute the following command:
+
+```bash
+Add-Migration "Created_Book_Entity"
+```
+
+
+
+This will create a new migration class inside the `Migrations` folder of the `Acme.BookStore.EntityFrameworkCore.DbMigrations` project. Then execute the `Update-Database` command to update the database schema:
+
+````bash
+Update-Database
+````
+
+
+
+#### Add initial (sample) data
+
+`Update-Database` command has created the `AppBooks` table in the database. Open your database and enter a few sample rows, so you can show them on the listing page.
+
+```mssql
+INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
+('f3c04764-6bfd-49e2-859e-3f9bfda6183e', '2018-07-01', '1984',3,'1949-06-08','19.84')
+
+INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
+('13024066-35c9-473c-997b-83cd8d3e29dc', '2018-07-01', 'The Hitchhiker`s Guide to the Galaxy',7,'1995-09-27','42')
+
+INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
+('4fa024a1-95ac-49c6-a709-6af9e4d54b54', '2018-07-02', 'Pet Sematary',5,'1983-11-14','23.7')
+```
+
+
+
+{{end}}
+
+### Create the application service
+
+The next step is to create an [application service](../../Application-Services.md) to manage the books which will allow us the four basic functions: creating, reading, updating and deleting. Application layer is separated into two projects:
+
+* `Acme.BookStore.Application.Contracts` mainly contains your `DTO`s and application service interfaces.
+* `Acme.BookStore.Application` contains the implementations of your application services.
+
+#### BookDto
+
+Create a DTO class named `BookDto` into the `Acme.BookStore.Application.Contracts` project:
+
+````csharp
+using System;
+using Volo.Abp.Application.Dtos;
+
+namespace Acme.BookStore
+{
+ public class BookDto : AuditedEntityDto
+ {
+ public string Name { get; set; }
+
+ public BookType Type { get; set; }
+
+ public DateTime PublishDate { get; set; }
+
+ public float Price { get; set; }
+ }
+}
+````
+
+* **DTO** classes are used to **transfer data** between the *presentation layer* and the *application layer*. See the [Data Transfer Objects document](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects) for more details.
+* `BookDto` is used to transfer book data to the presentation layer in order to show the book information on the UI.
+* `BookDto` is derived from the `AuditedEntityDto` which has audit properties just like the `Book` class defined above.
+
+It will be needed to map `Book` entities to `BookDto` objects while returning books to the presentation layer. [AutoMapper](https://automapper.org) library can automate this conversion when you define the proper mapping. The startup template comes with AutoMapper configured, so you can just define the mapping in the `BookStoreApplicationAutoMapperProfile` class in the `Acme.BookStore.Application` project:
+
+````csharp
+using AutoMapper;
+
+namespace Acme.BookStore
+{
+ public class BookStoreApplicationAutoMapperProfile : Profile
+ {
+ public BookStoreApplicationAutoMapperProfile()
+ {
+ CreateMap();
+ }
+ }
+}
+````
+
+#### CreateUpdateBookDto
+
+Create a DTO class named `CreateUpdateBookDto` into the `Acme.BookStore.Application.Contracts` project:
+
+````csharp
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Acme.BookStore
+{
+ public class CreateUpdateBookDto
+ {
+ [Required]
+ [StringLength(128)]
+ public string Name { get; set; }
+
+ [Required]
+ public BookType Type { get; set; } = BookType.Undefined;
+
+ [Required]
+ public DateTime PublishDate { get; set; }
+
+ [Required]
+ public float Price { get; set; }
+ }
+}
+````
+
+* This `DTO` class is used to get book information from the user interface while creating or updating a book.
+* It defines data annotation attributes (like `[Required]`) to define validations for the properties. `DTO`s are [automatically validated](https://docs.abp.io/en/abp/latest/Validation) by the ABP framework.
+
+Next, add a mapping in `BookStoreApplicationAutoMapperProfile` from the `CreateUpdateBookDto` object to the `Book` entity with the `CreateMap();` command:
+
+````csharp
+using AutoMapper;
+
+namespace Acme.BookStore
+{
+ public class BookStoreApplicationAutoMapperProfile : Profile
+ {
+ public BookStoreApplicationAutoMapperProfile()
+ {
+ CreateMap();
+ CreateMap(); //<--added this line-->
+ }
+ }
+}
+````
+
+#### IBookAppService
+
+Create an interface named `IBookAppService` in the `Acme.BookStore.Application.Contracts` project:
+
+````csharp
+using System;
+using Volo.Abp.Application.Dtos;
+using Volo.Abp.Application.Services;
+
+namespace Acme.BookStore
+{
+ public interface IBookAppService :
+ ICrudAppService< //Defines CRUD methods
+ BookDto, //Used to show books
+ Guid, //Primary key of the book entity
+ PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books
+ CreateUpdateBookDto, //Used to create a new book
+ CreateUpdateBookDto> //Used to update a book
+ {
+
+ }
+}
+````
+
+* Defining interfaces for the application services **are not required** by the framework. However, it's suggested as a best practice.
+* `ICrudAppService` defines common **CRUD** methods: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync` and `DeleteAsync`. It's not required to extend it. Instead, you could inherit from the empty `IApplicationService` interface and define your own methods manually.
+* There are some variations of the `ICrudAppService` where you can use separated DTOs for each method.
+
+#### BookAppService
+
+Implement the `IBookAppService` as named `BookAppService` in the `Acme.BookStore.Application` project:
+
+````csharp
+using System;
+using Volo.Abp.Application.Dtos;
+using Volo.Abp.Application.Services;
+using Volo.Abp.Domain.Repositories;
+
+namespace Acme.BookStore
+{
+ public class BookAppService :
+ CrudAppService,
+ IBookAppService
+ {
+ public BookAppService(IRepository repository)
+ : base(repository)
+ {
+
+ }
+ }
+}
+````
+
+* `BookAppService` is derived from `CrudAppService<...>` which implements all the CRUD (create, read, update, delete) methods defined above.
+* `BookAppService` injects `IRepository` which is the default repository for the `Book` entity. ABP automatically creates default repositories for each aggregate root (or entity). See the [repository document](https://docs.abp.io/en/abp/latest/Repositories).
+* `BookAppService` uses `IObjectMapper` to map `Book` objects to `BookDto` objects and `CreateUpdateBookDto` objects to `Book` objects. The Startup template uses the [AutoMapper](http://automapper.org/) library as the object mapping provider. We have defined the mappings before, so it will work as expected.
+
+### Auto API Controllers
+
+We normally create **Controllers** to expose application services as **HTTP API** endpoints. This allows browsers or 3rd-party clients to call them via AJAX. ABP can [**automagically**](https://docs.abp.io/en/abp/latest/AspNetCore/Auto-API-Controllers) configures your application services as MVC API Controllers by convention.
+
+#### Swagger UI
+
+The startup template is configured to run the [Swagger UI](https://swagger.io/tools/swagger-ui/) using the [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) library. Run the application by pressing `CTRL+F5` and navigate to `https://localhost:/swagger/` on your browser. (Replace `` with your own port number.)
+
+You will see some built-in service endpoints as well as the `Book` service and its REST-style endpoints:
+
+
+
+Swagger has a nice interface to test the APIs. You can try to execute the `[GET] /api/app/book` API to get a list of books.
+
+{{if UI == "MVC"}}
+
+### Dynamic JavaScript proxies
+
+It's common to call 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** for all API endpoints. So, you can use any **endpoint** just like calling a **JavaScript function**.
+
+#### Testing in developer console of the browser
+
+You can easily test the JavaScript proxies using your favorite browser's **Developer Console**. Run the application, open your browser's **developer tools** (*shortcut is F12 for Chrome*), switch to the **Console** tab, type the following code and press enter:
+
+````js
+acme.bookStore.book.getList({}).done(function (result) { console.log(result); });
+````
+
+* `acme.bookStore` is the namespace of the `BookAppService` converted to [camelCase](https://en.wikipedia.org/wiki/Camel_case).
+* `book` is the conventional name for the `BookAppService` (removed `AppService` postfix and converted to camelCase).
+* `getList` is the conventional name for the `GetListAsync` method defined in the `AsyncCrudAppService` base class (removed `Async` postfix and converted to camelCase).
+* `{}` argument is used to send an empty object to the `GetListAsync` method which normally expects an object of type `PagedAndSortedResultRequestDto` that is used to send paging and sorting options to the server (all properties are optional, so you can send an empty object).
+* `getList` function returns a `promise`. You can pass a callback to the `done` (or `then`) function to get the result from the server.
+
+Running this code produces the following output:
+
+
+
+You can see the **book list** returned from the server. You can also check the **network** tab of the developer tools to see the client to server communication:
+
+
+
+Let's **create a new book** using the `create` function:
+
+````js
+acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-24', price: 21.5 }).done(function (result) { console.log('successfully created the book with id: ' + result.id); });
+````
+
+You should see a message in the console something like that:
+
+````text
+successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
+````
+
+Check the `Books` table in the database to see the new book row. You can try `get`, `update` and `delete` functions yourself.
+
+### Create the books page
+
+It's time to create something visible and usable! Instead of classic MVC, we will use the new [Razor Pages UI](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start) approach which is recommended by Microsoft.
+
+Create `Books` folder under the `Pages` folder of the `Acme.BookStore.Web` project. Add a new Razor Page by right clicking the Books folder then selecting **Add > Razor Page** menu item. Name it as `Index`:
+
+
+
+Open the `Index.cshtml` and change the whole content as shown below:
+
+**Index.cshtml:**
+
+````html
+@page
+@using Acme.BookStore.Web.Pages.Books
+@inherits Acme.BookStore.Web.Pages.BookStorePage
+@model IndexModel
+
+
Books
+````
+
+* This code changes the default inheritance of the Razor View Page Model so it **inherits** from the `BookStorePage` class (instead of `PageModel`). The `BookStorePage` class which comes with the startup template, provides some shared properties/methods used by all pages.
+
+* Set the `IndexModel`'s namespace to `Acme.BookStore.Pages.Books` in `Index.cshtml.cs`.
+
+
+
+**Index.cshtml.cs:**
+
+```csharp
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace Acme.BookStore.Web.Pages.Books
+{
+ public class IndexModel : PageModel
+ {
+ public void OnGet()
+ {
+
+ }
+ }
+}
+```
+
+#### Add books page to the main menu
+
+Open the `BookStoreMenuContributor` class in the `Menus` folder and add the following code to the end of the `ConfigureMainMenuAsync` method:
+
+````csharp
+//...
+namespace Acme.BookStore.Web.Menus
+{
+ public class BookStoreMenuContributor : IMenuContributor
+ {
+ private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
+ {
+ //<-- added the below code
+ context.Menu.AddItem(
+ new ApplicationMenuItem("BooksStore", l["Menu:BookStore"])
+ .AddItem(
+ new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books")
+ )
+ );
+ //-->
+ }
+ }
+}
+````
+
+{{end}}
+
+#### Localize the menu items
+
+Localization texts are located under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project:
+
+
+
+Open the `en.json` (*English translations*) file and add the below localization texts to the end of the file:
+
+````json
+{
+ "Culture": "en",
+ "Texts": {
+ "Menu:Home": "Home",
+ "Welcome": "Welcome",
+ "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.",
+
+ "Menu:BookStore": "Book Store",
+ "Menu:Books": "Books",
+ "Actions": "Actions",
+ "Edit": "Edit",
+ "PublishDate": "Publish date",
+ "NewBook": "New book",
+ "Name": "Name",
+ "Type": "Type",
+ "Price": "Price",
+ "CreationTime": "Creation time",
+ "AreYouSureToDelete": "Are you sure you want to delete this item?"
+ }
+}
+````
+
+* ABP's localization system is built on [ASP.NET Core's standard localization](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) system and extends it in many ways. See the [localization document](https://docs.abp.io/en/abp/latest/Localization) for details.
+* Localization key names are arbitrary. You can set any name. As a best practice, we prefer to add `Menu:` prefix for menu items to distinguish from other texts. If a text is not defined in the localization file, it **fallbacks** to the localization key (as ASP.NET Core's standard behavior).
+
+{{if UI == "MVC"}}
+
+Run the project, login to the application with the username `admin` and password `1q2w3E*` and see the new menu item has been added to the menu.
+
+
+
+When you click to the Books menu item under the Book Store parent, you are being redirected to the new Books page.
+
+#### Book list
+
+We will use the [Datatables.net](https://datatables.net/) jQuery plugin to show the book list. [Datatables](https://datatables.net/) can completely work via AJAX, it is fast, popular and provides a good user experience. [Datatables](https://datatables.net/) plugin is configured in the startup template, so you can directly use it in any page without including any style or script file to your page.
+
+##### Index.cshtml
+
+Change the `Pages/Books/Index.cshtml` as following:
+
+````html
+@page
+@inherits Acme.BookStore.Web.Pages.BookStorePage
+@model Acme.BookStore.Web.Pages.Books.IndexModel
+@section scripts
+{
+
+}
+
+
+
@L["Books"]
+
+
+
+
+
+
@L["Name"]
+
@L["Type"]
+
@L["PublishDate"]
+
@L["Price"]
+
@L["CreationTime"]
+
+
+
+
+
+````
+
+* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro) is used to add external **scripts** to the page. It has many additional features compared to standard `script` tag. It handles **minification** and **versioning**. See the [bundling & minification document](https://docs.abp.io/en/abp/latest/AspNetCore/Bundling-Minification) for details.
+* `abp-card` and `abp-table` are **tag helpers** for Twitter Bootstrap's [card component](http://getbootstrap.com/docs/4.1/components/card/). There are other useful tag helpers in ABP to easily use most of the [bootstrap](https://getbootstrap.com/) components. You can also use regular HTML tags instead of these tag helpers, but using tag helpers reduces HTML code and prevents errors by help the of IntelliSense and compile time type checking. Further information, see the [tag helpers](https://docs.abp.io/en/abp/latest/AspNetCore/Tag-Helpers/Index) document.
+* You can **localize** the column names in the localization file as you did for the menu items above.
+
+##### Add a Script File
+
+Create `index.js` JavaScript file under the `Pages/Books/` folder:
+
+
+
+`index.js` content is shown below:
+
+````js
+$(function () {
+ var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({
+ ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
+ columnDefs: [
+ { data: "name" },
+ { data: "type" },
+ { data: "publishDate" },
+ { data: "price" },
+ { data: "creationTime" }
+ ]
+ }));
+});
+````
+
+* `abp.libs.datatables.createAjax` is a helper function to adapt ABP's dynamic JavaScript API proxies to [Datatable](https://datatables.net/)'s format.
+* `abp.libs.datatables.normalizeConfiguration` is another helper function. There's no requirement to use it, but it simplifies the [Datatables](https://datatables.net/) configuration by providing conventional values for missing options.
+* `acme.bookStore.book.getList` is the function to get list of books (as described in [dynamic JavaScript proxies](#Dynamic JavaScript proxies)).
+* See [Datatables documentation](https://datatables.net/manual/) for all configuration options.
+
+It's end of this part. The final UI of this work is shown as below:
+
+
+
+{{end}}
+
+{{if UI == "NG"}}
+
+### Angular development
+#### Create the books page
+
+It's time to create something visible and usable! There are some tools that we will use when developing ABP Angular frontend application:
+
+- [Angular CLI](https://angular.io/cli) will be used to create modules, components and services.
+- [NGXS](https://ngxs.gitbook.io/ngxs/) will be used as the state management library.
+- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) will be used as the UI component library.
+- [Visual Studio Code](https://code.visualstudio.com/) will be used as the code editor (you can use your favorite editor).
+
+#### Install NPM packages
+
+Open a new command line interface (terminal window) and go to your `angular` folder and then run `yarn` command to install NPM packages:
+
+```bash
+yarn
+```
+
+#### BooksModule
+
+Run the following command line to create a new module, named `BooksModule`:
+
+```bash
+yarn ng generate module books --route books --module app.module
+```
+
+
+
+#### Routing
+
+Open the `app-routing.module.ts` file in `src\app` folder. Add the new `import` and replace `books` path as shown below
+
+```js
+import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <==
+
+//...replaced original books path with the below
+{
+ path: 'books',
+ component: ApplicationLayoutComponent,
+ loadChildren: () => import('./books/books.module').then(m => m.BooksModule),
+ data: {
+ routes: {
+ name: '::Menu:Books',
+ iconClass: 'fas fa-book'
+ } as ABP.Route
+ },
+}
+```
+
+* The `ApplicationLayoutComponent` configuration sets the application layout to the new page. We added the `data` object. The `name` is the menu item name and the `iconClass` is the icon of the menu item.
+
+Run `yarn start` and wait for Angular to serve the application:
+
+```bash
+yarn start
+```
+
+Open the browser and navigate to http://localhost:4200/books. You'll see a blank page saying "*books works!*".
+
+
+
+#### Book list component
+
+Replace the `books.component.html` in the `app\books` folder with the following content:
+
+```html
+
+```
+
+Then run the command below on the terminal in the root folder to generate a new component, named book-list:
+
+```bash
+yarn ng generate component books/book-list
+```
+
+
+
+Open `books.module.ts` file in the `app\books` folder and replace the content as below:
+
+```js
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { BooksRoutingModule } from './books-routing.module';
+import { BooksComponent } from './books.component';
+import { BookListComponent } from './book-list/book-list.component';
+import { SharedModule } from '../shared/shared.module'; //<== added this line ==>
+
+@NgModule({
+ declarations: [BooksComponent, BookListComponent],
+ imports: [
+ CommonModule,
+ BooksRoutingModule,
+ SharedModule, //<== added this line ==>
+ ]
+})
+export class BooksModule { }
+```
+
+* We imported `SharedModule` and added to `imports` array.
+
+Open `books-routing.module.ts` file in the `app\books` folder and replace the content as below:
+
+```js
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { BooksComponent } from './books.component';
+import { BookListComponent } from './book-list/book-list.component'; //<== added this line ==>
+
+//<== replaced routes ==>
+const routes: Routes = [
+ {
+ path: '',
+ component: BooksComponent,
+ children: [{ path: '', component: BookListComponent }],
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class BooksRoutingModule { }
+```
+
+* We imported `BookListComponent` and replaced `routes` const.
+
+We'll see **book-list works!** text on the books page:
+
+
+
+#### Create BooksState
+
+Run the following command in the terminal to create a new state, named `BooksState`:
+
+
+
+```bash
+yarn ng generate ngxs-schematic:state books
+```
+
+* This command creates several new files and updates `app.modules.ts` file to import the `NgxsModule` with the new state.
+
+#### Get books data from backend
+
+Create data types to map the data from the backend (you can check Swagger UI or your backend API to see the data format).
+
+
+
+Open the `books.ts` file in the `app\store\models` folder and replace the content as below:
+
+```js
+export namespace Books {
+ export interface State {
+ books: Response;
+ }
+
+ export interface Response {
+ items: Book[];
+ totalCount: number;
+ }
+
+ export interface Book {
+ name: string;
+ type: BookType;
+ publishDate: string;
+ price: number;
+ lastModificationTime: string;
+ lastModifierId: string;
+ creationTime: string;
+ creatorId: string;
+ id: string;
+ }
+
+ export enum BookType {
+ Undefined,
+ Adventure,
+ Biography,
+ Dystopia,
+ Fantastic,
+ Horror,
+ Science,
+ ScienceFiction,
+ Poetry,
+ }
+}
+```
+
+* Added `Book` interface that represents a book object and `BookType` enum which represents a book category.
+
+#### BooksService
+
+Create a new service, named `BooksService` to perform `HTTP` calls to the server:
+
+```bash
+yarn ng generate service books/shared/books
+```
+
+
+
+Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below:
+
+```js
+import { Injectable } from '@angular/core';
+import { RestService } from '@abp/ng.core';
+import { Books } from '../../store/models';
+import { Observable } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class BooksService {
+ constructor(private restService: RestService) {}
+
+ get(): Observable {
+ return this.restService.request({
+ method: 'GET',
+ url: '/api/app/book'
+ });
+ }
+}
+```
+
+* We added the `get` method to get the list of books by performing an HTTP request to the related endpoint.
+
+Open the`books.actions.ts` file in `app\store\actions` folder and replace the content below:
+
+```js
+export class GetBooks {
+ static readonly type = '[Books] Get';
+}
+```
+
+#### Implement BooksState
+
+Open the `books.state.ts` file in `app\store\states` folder and replace the content below:
+
+```js
+import { State, Action, StateContext, Selector } from '@ngxs/store';
+import { GetBooks } from '../actions/books.actions';
+import { Books } from '../models/books';
+import { BooksService } from '../../books/shared/books.service';
+import { tap } from 'rxjs/operators';
+
+@State({
+ name: 'BooksState',
+ defaults: { books: {} } as Books.State,
+})
+export class BooksState {
+ @Selector()
+ static getBooks(state: Books.State) {
+ return state.books.items || [];
+ }
+
+ constructor(private booksService: BooksService) {}
+
+ @Action(GetBooks)
+ get(ctx: StateContext) {
+ return this.booksService.get().pipe(
+ tap(booksResponse => {
+ ctx.patchState({
+ books: booksResponse,
+ });
+ }),
+ );
+ }
+}
+```
+
+* We added the `GetBooks` action that retrieves the books data via `BooksService` and patches the state.
+* `NGXS` requires to return the observable without subscribing it in the get function.
+
+#### BookListComponent
+
+Open the `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below:
+
+```js
+import { Component, OnInit } from '@angular/core';
+import { Store, Select } from '@ngxs/store';
+import { BooksState } from '../../store/states';
+import { Observable } from 'rxjs';
+import { Books } from '../../store/models';
+import { GetBooks } from '../../store/actions';
+
+@Component({
+ selector: 'app-book-list',
+ templateUrl: './book-list.component.html',
+ styleUrls: ['./book-list.component.scss'],
+})
+export class BookListComponent implements OnInit {
+ @Select(BooksState.getBooks)
+ books$: Observable;
+
+ booksType = Books.BookType;
+
+ loading = false;
+
+ constructor(private store: Store) { }
+
+ ngOnInit() {
+ this.get();
+ }
+
+ get() {
+ this.loading = true;
+ this.store.dispatch(new GetBooks()).subscribe(() => {
+ this.loading = false;
+ });
+ }
+}
+```
+
+* We added the `get` function that updates store to get the books.
+* See the [Dispatching actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions) and [Select](https://ngxs.gitbook.io/ngxs/concepts/select) on the `NGXS` documentation for more information on these `NGXS` features.
+
+Open the `book-list.component.html` file in `app\books\book-list` folder and replace the content as below:
+
+```html
+
+
+
+
+
+ {%{{{ "::Menu:Books" | abpLocalization }}}%}
+
+
+
+
+
+
+
+
+
+
+
{%{{{ "::Name" | abpLocalization }}}%}
+
{%{{{ "::Type" | abpLocalization }}}%}
+
{%{{{ "::PublishDate" | abpLocalization }}}%}
+
{%{{{ "::Price" | abpLocalization }}}%}
+
+
+
+
+
{%{{{ data.name }}}%}
+
{%{{{ booksType[data.type] }}}%}
+
{%{{{ data.publishDate | date }}}%}
+
{%{{{ data.price }}}%}
+
+
+
+
+```
+
+* We added HTML code of book list page.
+
+Now you can see the final result on your browser:
+
+
+
+The file system structure of the project:
+
+
+
+In this tutorial we have applied the rules of official [Angular Style Guide](https://angular.io/guide/styleguide#file-tree).
+
+{{end}}
+
+### Next Part
+
+See the [part 2](part-2.md) for creating, updating and deleting books.
diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md
new file mode 100644
index 0000000000..8408cf6bfb
--- /dev/null
+++ b/docs/en/Tutorials/Part-2.md
@@ -0,0 +1,1447 @@
+## ASP.NET Core {{UI_Value}} Tutorial - Part 2
+````json
+//[doc-params]
+{
+ "UI": ["MVC","NG"]
+}
+````
+
+{{
+if UI == "MVC"
+ DB="ef"
+ DB_Text="Entity Framework Core"
+ UI_Text="mvc"
+else if UI == "NG"
+ DB="mongodb"
+ DB_Text="MongoDB"
+ UI_Text="angular"
+else
+ DB ="?"
+ UI_Text="?"
+end
+}}
+
+### About this tutorial
+
+This is the second part of the ASP.NET Core {{UI_Value}} tutorial series. All parts:
+
+* [Part I: Creating the project and book list page](part-1.md)
+* **Part II: Creating, updating and deleting books (this tutorial)**
+* [Part III: Integration tests](part-3.md)
+
+*You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial.*
+
+{{if UI == "MVC"}}
+
+### Creating a new book
+
+In this section, you will learn how to create a new modal dialog form to create a new book. The modal dialog will look like in the below image:
+
+
+
+#### Create the modal form
+
+Create a new razor page, named `CreateModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project.
+
+
+
+##### CreateModal.cshtml.cs
+
+Open the `CreateModal.cshtml.cs` file (`CreateModalModel` class) and replace with the following code:
+
+````C#
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Acme.BookStore.Web.Pages.Books
+{
+ public class CreateModalModel : BookStorePageModel
+ {
+ [BindProperty]
+ public CreateUpdateBookDto Book { get; set; }
+
+ private readonly IBookAppService _bookAppService;
+
+ public CreateModalModel(IBookAppService bookAppService)
+ {
+ _bookAppService = bookAppService;
+ }
+
+ public async Task OnPostAsync()
+ {
+ await _bookAppService.CreateAsync(Book);
+ return NoContent();
+ }
+ }
+}
+````
+
+* This class is derived from the `BookStorePageModel` instead of standard `PageModel`. `BookStorePageModel` inherits the `PageModel` and adds some common properties & methods that can be used in your page model classes.
+* `[BindProperty]` attribute on the `Book` property binds post request data to this property.
+* This class simply injects the `IBookAppService` in the constructor and calls the `CreateAsync` method in the `OnPostAsync` handler.
+
+##### CreateModal.cshtml
+
+Open the `CreateModal.cshtml` file and paste the code below:
+
+````html
+@page
+@inherits Acme.BookStore.Web.Pages.BookStorePage
+@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
+@model Acme.BookStore.Web.Pages.Books.CreateModalModel
+@{
+ Layout = null;
+}
+
+
+
+
+
+
+
+
+
+````
+
+* This modal uses `abp-dynamic-form` tag helper to automatically create the form from the model `CreateBookViewModel`.
+ * `abp-model` attribute indicates the model object where it's the `Book` property in this case.
+ * `data-ajaxForm` attribute sets the form to submit via AJAX, instead of a classic page post.
+ * `abp-form-content` tag helper is a placeholder to render the form controls (it is optional and needed only if you have added some other content in the `abp-dynamic-form` tag, just like in this page).
+
+#### Add the "New book" button
+
+Open the `Pages/Books/Index.cshtml` and set the content of `abp-card-header` tag as below:
+
+````html
+
+
+
+
@L["Books"]
+
+
+
+
+
+
+````
+
+This adds a new button called **New book** to the **top-right** of the table:
+
+
+
+Open the `pages/books/index.js` and add the following code just after the `Datatable` configuration:
+
+````js
+var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
+
+createModal.onResult(function () {
+ dataTable.ajax.reload();
+});
+
+$('#NewBookButton').click(function (e) {
+ e.preventDefault();
+ createModal.open();
+});
+````
+
+* `abp.ModalManager` is a helper class to manage modals in the client side. It internally uses Twitter Bootstrap's standard modal, but abstracts many details by providing a simple API.
+
+Now, you can **run the application** and add new books using the new modal form.
+
+### Updating a book
+
+Create a new razor page, named `EditModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project:
+
+
+
+#### EditModal.cshtml.cs
+
+Open the `EditModal.cshtml.cs` file (`EditModalModel` class) and replace with the following code:
+
+````csharp
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Acme.BookStore.Web.Pages.Books
+{
+ public class EditModalModel : BookStorePageModel
+ {
+ [HiddenInput]
+ [BindProperty(SupportsGet = true)]
+ public Guid Id { get; set; }
+
+ [BindProperty]
+ public CreateUpdateBookDto 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();
+ }
+ }
+}
+````
+
+* `[HiddenInput]` and `[BindProperty]` are standard ASP.NET Core MVC attributes. `SupportsGet` is used to be able to get `Id` value from query string parameter of the request.
+* In the `GetAsync` method, we get `BookDto `from `BookAppService` and this is being mapped to the DTO object `CreateUpdateBookDto`.
+* The `OnPostAsync` uses `BookAppService.UpdateAsync()` to update the entity.
+
+#### Mapping from BookDto to CreateUpdateBookDto
+
+To be able to map the `BookDto` to `CreateUpdateBookDto`, configure a new mapping. To do this, open the `BookStoreWebAutoMapperProfile.cs` in the `Acme.BookStore.Web` project and change it as shown below:
+
+````csharp
+using AutoMapper;
+
+namespace Acme.BookStore.Web
+{
+ public class BookStoreWebAutoMapperProfile : Profile
+ {
+ public BookStoreWebAutoMapperProfile()
+ {
+ CreateMap();
+ }
+ }
+}
+````
+
+* We have just added `CreateMap();` to define this mapping.
+
+#### EditModal.cshtml
+
+Replace `EditModal.cshtml` content with the following content:
+
+````html
+@page
+@inherits Acme.BookStore.Web.Pages.BookStorePage
+@using Acme.BookStore.Web.Pages.Books
+@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
+@model EditModalModel
+@{
+ Layout = null;
+}
+
+
+
+
+
+
+
+
+
+
+````
+
+This page is very similar to the `CreateModal.cshtml`, except:
+
+* It includes an `abp-input` for the `Id` property to store `Id` of the editing book (which is a hidden input).
+* It uses `Books/EditModal` as the post URL and *Update* text as the modal header.
+
+#### Add "Actions" dropdown to the table
+
+We will add a dropdown button to the table named *Actions*.
+
+Open the `Pages/Books/Index.cshtml` page and change the `` section as shown below:
+
+````html
+
+
+
+
@L["Actions"]
+
@L["Name"]
+
@L["Type"]
+
@L["PublishDate"]
+
@L["Price"]
+
@L["CreationTime"]
+
+
+
+````
+
+* We just added a new `th` tag for the "*Actions*" button.
+
+Open the `pages/books/index.js` and replace the content as below:
+
+````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({
+ processing: true,
+ serverSide: true,
+ paging: true,
+ searching: false,
+ autoWidth: false,
+ scrollCollapse: true,
+ order: [[1, "asc"]],
+ ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
+ columnDefs: [
+ {
+ rowAction: {
+ items:
+ [
+ {
+ text: l('Edit'),
+ action: function (data) {
+ editModal.open({ id: data.record.id });
+ }
+ }
+ ]
+ }
+ },
+ { data: "name" },
+ { data: "type" },
+ { data: "publishDate" },
+ { data: "price" },
+ { data: "creationTime" }
+ ]
+ }));
+
+ createModal.onResult(function () {
+ dataTable.ajax.reload();
+ });
+
+ editModal.onResult(function () {
+ dataTable.ajax.reload();
+ });
+
+ $('#NewBookButton').click(function (e) {
+ e.preventDefault();
+ createModal.open();
+ });
+});
+````
+
+* Used `abp.localization.getResource('BookStore')` to be able to use the same localization texts defined on the server-side.
+* Added a new `ModalManager` named `createModal` to open the create modal dialog.
+* Added a new `ModalManager` named `editModal` to open the edit modal dialog.
+* Added a new column at the beginning of the `columnDefs` section. This column is used for the "*Actions*" dropdown button.
+* "*New Book*" action simply calls `createModal.open()` to open the create dialog.
+* "*Edit*" action simply calls `editModal.open()` to open the edit dialog.
+
+You can run the application and edit any book by selecting the edit action. The final UI looks as below:
+
+
+
+### Deleting a book
+
+Open the `pages/books/index.js` and add a new item to the `rowAction` `items`:
+
+````js
+{
+ text: l('Delete'),
+ confirmMessage: function (data) {
+ return l('BookDeletionConfirmationMessage', data.record.name);
+ },
+ action: function (data) {
+ acme.bookStore.book
+ .delete(data.record.id)
+ .then(function() {
+ abp.notify.info(l('SuccessfullyDeleted'));
+ dataTable.ajax.reload();
+ });
+ }
+}
+````
+
+* `confirmMessage` option is used to ask a confirmation question before executing the `action`.
+* `acme.bookStore.book.delete()` method makes an AJAX request to JavaScript proxy function to delete a book.
+* `abp.notify.info()` shows a notification after the delete operation.
+
+The final `index.js` content is shown below:
+
+````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({
+ processing: true,
+ serverSide: true,
+ paging: true,
+ searching: false,
+ autoWidth: false,
+ scrollCollapse: true,
+ order: [[1, "asc"]],
+ ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
+ columnDefs: [
+ {
+ rowAction: {
+ items:
+ [
+ {
+ text: l('Edit'),
+ action: function (data) {
+ editModal.open({ id: data.record.id });
+ }
+ },
+ {
+ text: l('Delete'),
+ confirmMessage: function (data) {
+ return l('BookDeletionConfirmationMessage', data.record.name);
+ },
+ action: function (data) {
+ acme.bookStore.book
+ .delete(data.record.id)
+ .then(function() {
+ abp.notify.info(l('SuccessfullyDeleted'));
+ dataTable.ajax.reload();
+ });
+ }
+ }
+ ]
+ }
+ },
+ { data: "name" },
+ { data: "type" },
+ { data: "publishDate" },
+ { data: "price" },
+ { data: "creationTime" }
+ ]
+ }));
+
+ createModal.onResult(function () {
+ dataTable.ajax.reload();
+ });
+
+ editModal.onResult(function () {
+ dataTable.ajax.reload();
+ });
+
+ $('#NewBookButton').click(function (e) {
+ e.preventDefault();
+ createModal.open();
+ });
+});
+````
+
+Open the `en.json` in the `Acme.BookStore.Domain.Shared` project and add the following translations:
+
+````json
+"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?",
+"SuccessfullyDeleted": "Successfully deleted"
+````
+
+Run the application and try to delete a book.
+
+{{end}}
+
+{{if UI == "NG"}}
+
+### Creating a new book
+
+In this section, you will learn how to create a new modal dialog form to create a new book.
+
+#### Type definition
+
+Open `books.ts` file in `app\store\models` folder and replace the content as below:
+
+```js
+export namespace Books {
+ export interface State {
+ books: Response;
+ }
+
+ export interface Response {
+ items: Book[];
+ totalCount: number;
+ }
+
+ export interface Book {
+ name: string;
+ type: BookType;
+ publishDate: string;
+ price: number;
+ lastModificationTime: string;
+ lastModifierId: string;
+ creationTime: string;
+ creatorId: string;
+ id: string;
+ }
+
+ export enum BookType {
+ Undefined,
+ Adventure,
+ Biography,
+ Dystopia,
+ Fantastic,
+ Horror,
+ Science,
+ ScienceFiction,
+ Poetry,
+ }
+
+ //<== added CreateUpdateBookInput interface ==>
+ export interface CreateUpdateBookInput {
+ name: string;
+ type: BookType;
+ publishDate: string;
+ price: number;
+ }
+}
+```
+
+* We added `CreateUpdateBookInput` interface.
+* You can see the properties of this interface from Swagger UI.
+* The `CreateUpdateBookInput` interface matches with the `CreateUpdateBookDto` in the backend.
+
+#### Service method
+
+Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below:
+
+```js
+import { Injectable } from '@angular/core';
+import { RestService } from '@abp/ng.core';
+import { Books } from '../../store/models';
+import { Observable } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class BooksService {
+ constructor(private restService: RestService) {}
+
+ get(): Observable {
+ return this.restService.request({
+ method: 'GET',
+ url: '/api/app/book'
+ });
+ }
+
+ //<== added create method ==>
+ create(createBookInput: Books.CreateUpdateBookInput): Observable {
+ return this.restService.request({
+ method: 'POST',
+ url: '/api/app/book',
+ body: createBookInput
+ });
+ }
+}
+```
+
+- We added the `create` method to perform an HTTP Post request to the server.
+- `restService.request` function gets generic parameters for the types sent to and received from the server. This example sends a `CreateUpdateBookInput` object and receives a `Book` object (you can set `void` for request or return type if not used).
+
+#### State definitions
+
+Open `books.action.ts` in `app\store\actions` folder and replace the content as below:
+
+```js
+import { Books } from '../models'; //<== added this line ==>
+
+export class GetBooks {
+ static readonly type = '[Books] Get';
+}
+
+//added CreateUpdateBook class
+export class CreateUpdateBook {
+ static readonly type = '[Books] Create Update Book';
+ constructor(public payload: Books.CreateUpdateBookInput) { }
+}
+```
+
+* We imported the Books namespace and created the `CreateUpdateBook` action.
+
+Open `books.state.ts` file in `app\store\states` and replace the content as below:
+
+```js
+import { State, Action, StateContext, Selector } from '@ngxs/store';
+import { GetBooks, CreateUpdateBook } from '../actions/books.actions'; //<== added CreateUpdateBook==>
+import { Books } from '../models/books';
+import { BooksService } from '../../books/shared/books.service';
+import { tap } from 'rxjs/operators';
+
+@State({
+ name: 'BooksState',
+ defaults: { books: {} } as Books.State,
+})
+export class BooksState {
+ @Selector()
+ static getBooks(state: Books.State) {
+ return state.books.items || [];
+ }
+
+ constructor(private booksService: BooksService) { }
+
+ @Action(GetBooks)
+ get(ctx: StateContext) {
+ return this.booksService.get().pipe(
+ tap(booksResponse => {
+ ctx.patchState({
+ books: booksResponse,
+ });
+ }),
+ );
+ }
+
+ //added CreateUpdateBook action listener
+ @Action(CreateUpdateBook)
+ save(ctx: StateContext, action: CreateUpdateBook) {
+ return this.booksService.create(action.payload);
+ }
+}
+```
+
+* We imported `CreateUpdateBook` action and defined the `save` method that will listen to a `CreateUpdateBook` action to create a book.
+
+When the `SaveBook` action dispatched, the save method is being executed. It calls `create` method of the `BooksService`.
+
+#### Add a modal to BookListComponent
+
+Open `book-list.component.html` file in `books\book-list` folder and replace the content as below:
+
+```html
+
+
+
+
+
+ {%{{{ '::Menu:Books' | abpLocalization }}}%}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{%{{{ "::Name" | abpLocalization }}}%}
+
{%{{{ "::Type" | abpLocalization }}}%}
+
{%{{{ "::PublishDate" | abpLocalization }}}%}
+
{%{{{ "::Price" | abpLocalization }}}%}
+
+
+
+
+
{%{{{ data.name }}}%}
+
{%{{{ booksType[data.type] }}}%}
+
{%{{{ data.publishDate | date }}}%}
+
{%{{{ data.price }}}%}
+
+
+
+
+
+
+
+
+
{%{{{ '::NewBook' | abpLocalization }}}%}
+
+
+
+
+
+
+
+
+```
+
+* We added the `abp-modal` which renders a modal to allow user to create a new book.
+* `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits.
+* We added `New book` button to the `AbpContentToolbar`.
+
+Open `book-list.component.` file in `books\book-list` folder and replace the content as below:
+
+```js
+import { Component, OnInit } from '@angular/core';
+import { Store, Select } from '@ngxs/store';
+import { BooksState } from '../../store/states';
+import { Observable } from 'rxjs';
+import { Books } from '../../store/models';
+import { GetBooks } from '../../store/actions';
+
+@Component({
+ selector: 'app-book-list',
+ templateUrl: './book-list.component.html',
+ styleUrls: ['./book-list.component.scss'],
+})
+export class BookListComponent implements OnInit {
+ @Select(BooksState.getBooks)
+ books$: Observable;
+
+ booksType = Books.BookType;
+
+ loading = false;
+
+ isModalOpen = false; //<== added this line ==>
+
+ constructor(private store: Store) { }
+
+ ngOnInit() {
+ this.get();
+ }
+
+ get() {
+ this.loading = true;
+ this.store.dispatch(new GetBooks()).subscribe(() => {
+ this.loading = false;
+ });
+ }
+
+ //added createBook method
+ createBook() {
+ this.isModalOpen = true;
+ }
+}
+```
+
+* We added `isModalOpen = false` and `createBook` method.
+
+You can open your browser and click **New book** button to see the new modal.
+
+
+
+#### Create a reactive form
+
+[Reactive forms](https://angular.io/guide/reactive-forms) provide a model-driven approach to handling form inputs whose values change over time.
+
+Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below:
+
+```js
+import { Component, OnInit } from '@angular/core';
+import { Store, Select } from '@ngxs/store';
+import { BooksState } from '../../store/states';
+import { Observable } from 'rxjs';
+import { Books } from '../../store/models';
+import { GetBooks } from '../../store/actions';
+import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added this line ==>
+
+@Component({
+ selector: 'app-book-list',
+ templateUrl: './book-list.component.html',
+ styleUrls: ['./book-list.component.scss'],
+})
+export class BookListComponent implements OnInit {
+ @Select(BooksState.getBooks)
+ books$: Observable;
+
+ booksType = Books.BookType;
+
+ loading = false;
+
+ isModalOpen = false;
+
+ form: FormGroup;
+
+ constructor(private store: Store, private fb: FormBuilder) { } //<== added FormBuilder ==>
+
+ ngOnInit() {
+ this.get();
+ }
+
+ get() {
+ this.loading = true;
+ this.store.dispatch(new GetBooks()).subscribe(() => {
+ this.loading = false;
+ });
+ }
+
+ createBook() {
+ this.buildForm(); //<== added this line ==>
+ this.isModalOpen = true;
+ }
+
+ //added buildForm method
+ buildForm() {
+ this.form = this.fb.group({
+ name: ['', Validators.required],
+ type: [null, Validators.required],
+ publishDate: [null, Validators.required],
+ price: [null, Validators.required],
+ });
+ }
+}
+```
+
+* We imported `FormGroup, FormBuilder and Validators`.
+* We injected `fb: FormBuilder` service to the constructor. The [FormBuilder](https://angular.io/api/forms/FormBuilder) service provides convenient methods for generating controls. It reduces the amount of boilerplate needed to build complex forms.
+* We added `buildForm` method to the end of the file and executed `buildForm()` in the `createBook` method. This method creates a reactive form to be able to create a new book.
+ * The `group` method of `FormBuilder`, `fb` creates a `FormGroup`.
+ * Added `Validators.required` static method which validates the relevant form element.
+
+#### Create the DOM elements of the form
+
+Open `book-list.component.html` in `app\books\book-list` folder and replace `` with the following code part:
+
+```html
+
+
+
+```
+
+- This template creates a form with `Name`, `Price`, `Type` and `Publish` date fields.
+- We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component.
+
+#### Datepicker requirements
+
+Open `books.module.ts` file in `app\books` folder and replace the content as below:
+
+```js
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { BooksRoutingModule } from './books-routing.module';
+import { BooksComponent } from './books.component';
+import { BookListComponent } from './book-list/book-list.component';
+import { SharedModule } from '../shared/shared.module';
+import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==>
+
+@NgModule({
+ declarations: [BooksComponent, BookListComponent],
+ imports: [
+ CommonModule,
+ BooksRoutingModule,
+ SharedModule,
+ NgbDatepickerModule //<== added this line ==>
+ ]
+})
+export class BooksModule { }
+```
+
+* We imported `NgbDatepickerModule` to be able to use the date picker.
+
+
+
+Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below:
+
+```js
+import { Component, OnInit } from '@angular/core';
+import { Store, Select } from '@ngxs/store';
+import { BooksState } from '../../store/states';
+import { Observable } from 'rxjs';
+import { Books } from '../../store/models';
+import { GetBooks } from '../../store/actions';
+import { FormGroup, FormBuilder, Validators } from '@angular/forms';
+import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==>
+
+@Component({
+ selector: 'app-book-list',
+ templateUrl: './book-list.component.html',
+ styleUrls: ['./book-list.component.scss'],
+ providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] //<== added this line ==>
+})
+export class BookListComponent implements OnInit {
+ @Select(BooksState.getBooks)
+ books$: Observable;
+
+ booksType = Books.BookType;
+
+ //added bookTypeArr array
+ bookTypeArr = Object.keys(Books.BookType).filter(
+ bookType => typeof this.booksType[bookType] === 'number'
+ );
+
+ loading = false;
+
+ isModalOpen = false;
+
+ form: FormGroup;
+
+ constructor(private store: Store, private fb: FormBuilder) { }
+
+ ngOnInit() {
+ this.get();
+ }
+
+ get() {
+ this.loading = true;
+ this.store.dispatch(new GetBooks()).subscribe(() => {
+ this.loading = false;
+ });
+ }
+
+ createBook() {
+ this.buildForm();
+ this.isModalOpen = true;
+ }
+
+ buildForm() {
+ this.form = this.fb.group({
+ name: ['', Validators.required],
+ type: [null, Validators.required],
+ publishDate: [null, Validators.required],
+ price: [null, Validators.required],
+ });
+ }
+}
+```
+
+* We imported ` NgbDateNativeAdapter, NgbDateAdapter`
+
+* We added a new provider `NgbDateAdapter` that converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details.
+
+* We added `bookTypeArr` array to be able to use it in the combobox values. The `bookTypeArr` contains the fields of the `BookType` enum. Resulting array is shown below:
+
+ ```js
+ ['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...]
+ ```
+
+ This array was used in the previous form template in the `ngFor` loop.
+
+Now, you can open your browser to see the changes:
+
+
+
+
+#### Saving the book
+
+Open `book-list.component.html` in `app\books\book-list` folder and add the following `abp-button` to save the new book.
+
+```html
+
+
+
+
+
+
+```
+
+* This adds a save button to the bottom area of the modal:
+
+
+
+Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below:
+
+```js
+import { Component, OnInit } from '@angular/core';
+import { Store, Select } from '@ngxs/store';
+import { BooksState } from '../../store/states';
+import { Observable } from 'rxjs';
+import { Books } from '../../store/models';
+import { GetBooks, CreateUpdateBook } from '../../store/actions'; //<== added CreateUpdateBook ==>
+import { FormGroup, FormBuilder, Validators } from '@angular/forms';
+import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+ selector: 'app-book-list',
+ templateUrl: './book-list.component.html',
+ styleUrls: ['./book-list.component.scss'],
+ providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }]
+})
+export class BookListComponent implements OnInit {
+ @Select(BooksState.getBooks)
+ books$: Observable;
+
+ booksType = Books.BookType;
+
+ bookTypeArr = Object.keys(Books.BookType).filter(
+ bookType => typeof this.booksType[bookType] === 'number'
+ );
+
+ loading = false;
+
+ isModalOpen = false;
+
+ form: FormGroup;
+
+ constructor(private store: Store, private fb: FormBuilder) { }
+
+ ngOnInit() {
+ this.get();
+ }
+
+ get() {
+ this.loading = true;
+ this.store.dispatch(new GetBooks()).subscribe(() => {
+ this.loading = false;
+ });
+ }
+
+ createBook() {
+ this.buildForm();
+ this.isModalOpen = true;
+ }
+
+ buildForm() {
+ this.form = this.fb.group({
+ name: ['', Validators.required],
+ type: [null, Validators.required],
+ publishDate: [null, Validators.required],
+ price: [null, Validators.required],
+ });
+ }
+
+ //<== added save ==>
+ save() {
+ if (this.form.invalid) {
+ return;
+ }
+
+ this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => {
+ this.isModalOpen = false;
+ this.form.reset();
+ this.get();
+ });
+ }
+}
+```
+
+* We imported `CreateUpdateBook`.
+* We added `save` method
+
+### Updating an existing book
+
+#### BooksService
+
+Open the `books.service.ts` in `app\books\shared` folder and add the `getById` and `update` methods.
+
+```js
+getById(id: string): Observable {
+ return this.restService.request({
+ method: 'GET',
+ url: `/api/app/book/${id}`
+ });
+}
+
+update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable {
+ return this.restService.request({
+ method: 'PUT',
+ url: `/api/app/book/${id}`,
+ body: updateBookInput
+ });
+}
+```
+
+#### CreateUpdateBook action
+
+Open the `books.actions.ts` in `app\store\actions` folder and replace the content as below:
+
+```js
+import { Books } from '../models';
+
+export class GetBooks {
+ static readonly type = '[Books] Get';
+}
+
+export class CreateUpdateBook {
+ static readonly type = '[Books] Create Update Book';
+ constructor(public payload: Books.CreateUpdateBookInput, public id?: string) { } //<== added id parameter ==>
+}
+```
+
+* We added `id` parameter to the `CreateUpdateBook` action's constructor.
+
+Open the `books.state.ts` in `app\store\states` folder and replace the `save` method as below:
+
+```js
+@Action(CreateUpdateBook)
+save(ctx: StateContext, action: CreateUpdateBook) {
+ if (action.id) {
+ return this.booksService.update(action.payload, action.id);
+ } else {
+ return this.booksService.create(action.payload);
+ }
+}
+```
+
+#### BookListComponent
+
+Open `book-list.component.ts` in `app\books\book-list` folder and inject `BooksService` dependency by adding it to the constructor and add a variable named `selectedBook`.
+
+```js
+import { Component, OnInit } from '@angular/core';
+import { Store, Select } from '@ngxs/store';
+import { BooksState } from '../../store/states';
+import { Observable } from 'rxjs';
+import { Books } from '../../store/models';
+import { GetBooks, CreateUpdateBook } from '../../store/actions';
+import { FormGroup, FormBuilder, Validators } from '@angular/forms';
+import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
+import { BooksService } from '../shared/books.service'; //<== imported BooksService ==>
+
+@Component({
+ selector: 'app-book-list',
+ templateUrl: './book-list.component.html',
+ styleUrls: ['./book-list.component.scss'],
+ providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }]
+})
+export class BookListComponent implements OnInit {
+ @Select(BooksState.getBooks)
+ books$: Observable;
+
+ booksType = Books.BookType;
+
+ bookTypeArr = Object.keys(Books.BookType).filter(
+ bookType => typeof this.booksType[bookType] === 'number'
+ );
+
+ loading = false;
+
+ isModalOpen = false;
+
+ form: FormGroup;
+
+ selectedBook = {} as Books.Book; //<== declared selectedBook ==>
+
+ constructor(private store: Store, private fb: FormBuilder, private booksService: BooksService) { }
+
+ ngOnInit() {
+ this.get();
+ }
+
+ get() {
+ this.loading = true;
+ this.store.dispatch(new GetBooks()).subscribe(() => {
+ this.loading = false;
+ });
+ }
+
+ //<== this method is replaced ==>
+ createBook() {
+ this.selectedBook = {} as Books.Book; //<== added ==>
+ this.buildForm();
+ this.isModalOpen = true;
+ }
+
+ //<== added editBook method ==>
+ editBook(id: string) {
+ this.booksService.getById(id).subscribe(book => {
+ this.selectedBook = book;
+ this.buildForm();
+ this.isModalOpen = true;
+ });
+ }
+
+ //<== this method is replaced ==>
+ buildForm() {
+ this.form = this.fb.group({
+ name: [this.selectedBook.name || "", Validators.required],
+ type: [this.selectedBook.type || null, Validators.required],
+ publishDate: [
+ this.selectedBook.publishDate
+ ? new Date(this.selectedBook.publishDate)
+ : null,
+ Validators.required
+ ],
+ price: [this.selectedBook.price || null, Validators.required]
+ });
+ }
+
+ save() {
+ if (this.form.invalid) {
+ return;
+ }
+
+ //<== added this.selectedBook.id ==>
+ this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id))
+ .subscribe(() => {
+ this.isModalOpen = false;
+ this.form.reset();
+ this.get();
+ });
+ }
+}
+```
+
+* We imported `BooksService`.
+* We declared a variable named `selectedBook` as `Books.Book`.
+* We injected `BooksService` to the constructor. `BooksService` is being used to retrieve the book data which is being edited.
+* We added `editBook` method. This method fetches the book with the given `Id` and sets it to `selectedBook` object.
+* We replaced the `buildForm` method so that it creates the form with the `selectedBook` data.
+* We replaced the `createBook` method so it sets `selectedBook` to an empty object.
+* We added `selectedBook.id` to the constructor of the new `CreateUpdateBook`.
+
+#### Add "Actions" dropdown to the table
+
+Open the `book-list.component.html` in `app\books\book-list` folder and replace the `
` tag as below:
+
+```html
+
+
+
+
+
+
{%{{{ "::Actions" | abpLocalization }}}%}
+
{%{{{ "::Name" | abpLocalization }}}%}
+
{%{{{ "::Type" | abpLocalization }}}%}
+
{%{{{ "::PublishDate" | abpLocalization }}}%}
+
{%{{{ "::Price" | abpLocalization }}}%}
+
+
+
+
+
+
+
+
+
+
+
+
+
{%{{{ data.name }}}%}
+
{%{{{ booksType[data.type] }}}%}
+
{%{{{ data.publishDate | date }}}%}
+
{%{{{ data.price }}}%}
+
+
+
+```
+
+- We added a `th` for the "Actions" column.
+- We added `button` with `ngbDropdownToggle` to open actions when clicked the button.
+- We have used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions.
+
+The final UI looks like as below:
+
+
+
+Open `book-list.component.html` in `app\books\book-list` folder and find the `` tag and replace the content as below.
+
+```html
+
+
+
+```
+
+* This template will show **Edit** text for edit record operation, **New Book** for new record operation in the title.
+
+### Deleting a book
+
+#### BooksService
+
+Open `books.service.ts` in `app\books\shared` folder and add the below `delete` method to delete a book.
+
+```js
+delete(id: string): Observable {
+ return this.restService.request({
+ method: 'DELETE',
+ url: `/api/app/book/${id}`
+ });
+}
+```
+
+* `Delete` method gets `id` parameter and makes a `DELETE` HTTP request to the relevant endpoint.
+
+#### DeleteBook action
+
+Open `books.actions.ts` in `app\store\actions `folder and add an action named `DeleteBook`.
+
+```js
+export class DeleteBook {
+ static readonly type = '[Books] Delete';
+ constructor(public id: string) {}
+}
+```
+
+Open the `books.state.ts` in `app\store\states` folder and replace the content as below:
+
+```js
+import { State, Action, StateContext, Selector } from '@ngxs/store';
+import { GetBooks, CreateUpdateBook, DeleteBook } from '../actions/books.actions'; //<== added DeleteBook==>
+import { Books } from '../models/books';
+import { BooksService } from '../../books/shared/books.service';
+import { tap } from 'rxjs/operators';
+
+@State({
+ name: 'BooksState',
+ defaults: { books: {} } as Books.State,
+})
+export class BooksState {
+ @Selector()
+ static getBooks(state: Books.State) {
+ return state.books.items || [];
+ }
+
+ constructor(private booksService: BooksService) { }
+
+ @Action(GetBooks)
+ get(ctx: StateContext) {
+ return this.booksService.get().pipe(
+ tap(booksResponse => {
+ ctx.patchState({
+ books: booksResponse,
+ });
+ }),
+ );
+ }
+
+ @Action(CreateUpdateBook)
+ save(ctx: StateContext, action: CreateUpdateBook) {
+ if (action.id) {
+ return this.booksService.update(action.payload, action.id);
+ } else {
+ return this.booksService.create(action.payload);
+ }
+ }
+
+ //<== added DeleteBook ==>
+ @Action(DeleteBook)
+ delete(ctx: StateContext, action: DeleteBook) {
+ return this.booksService.delete(action.id);
+ }
+}
+```
+
+- We imported `DeleteBook` .
+
+- We added `DeleteBook` action listener to the end of the file.
+
+
+
+#### Add a delete button
+
+
+Open `book-list.component.html` in `app\books\book-list` folder and modify the `ngbDropdownMenu` to add the delete button as shown below:
+
+```html
+
+
+
+
+```
+
+The final actions dropdown UI looks like below:
+
+
+
+#### Delete confirmation dialog
+
+Open `book-list.component.ts` in`app\books\book-list` folder and inject the `ConfirmationService`.
+
+Replace the constructor as below:
+
+```js
+import { ConfirmationService } from '@abp/ng.theme.shared';
+//...
+
+constructor(
+ private store: Store, private fb: FormBuilder,
+ private booksService: BooksService,
+ private confirmationService: ConfirmationService // <== added this line ==>
+) { }
+```
+
+* We imported `ConfirmationService`.
+* We injected `ConfirmationService` to the constructor.
+
+In the `book-list.component.ts` add a delete method :
+
+```js
+import { GetBooks, CreateUpdateBook, DeleteBook } from '../../store/actions'; //<== added DeleteBook ==>
+
+import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== added Confirmation ==>
+
+//...
+
+delete(id: string, name: string) {
+ this.confirmationService
+ .warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure')
+ .subscribe(status => {
+ if (status === Confirmation.Status.confirm) {
+ this.store.dispatch(new DeleteBook(id)).subscribe(() => this.get());
+ }
+ });
+}
+```
+
+The `delete` method shows a confirmation popup and subscribes for the user response. `DeleteBook` action dispatched only if user clicks to the `Yes` button. The confirmation popup looks like below:
+
+
+
+{{end}}
+
+### Next Part
+
+See the [next part](part-3.md) of this tutorial.
diff --git a/docs/en/Tutorials/images/bookstore-actions-buttons.png b/docs/en/Tutorials/images/bookstore-actions-buttons.png
new file mode 100644
index 0000000000..e8243fedc7
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-actions-buttons.png differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png b/docs/en/Tutorials/images/bookstore-add-create-dialog-v2.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png
rename to docs/en/Tutorials/images/bookstore-add-create-dialog-v2.png
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png b/docs/en/Tutorials/images/bookstore-add-edit-dialog.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png
rename to docs/en/Tutorials/images/bookstore-add-edit-dialog.png
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png b/docs/en/Tutorials/images/bookstore-add-index-page-v2.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png
rename to docs/en/Tutorials/images/bookstore-add-index-page-v2.png
diff --git a/docs/en/Tutorials/images/bookstore-angular-file-tree.png b/docs/en/Tutorials/images/bookstore-angular-file-tree.png
new file mode 100644
index 0000000000..28e570f604
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-angular-file-tree.png differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png b/docs/en/Tutorials/images/bookstore-appservice-tests.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png
rename to docs/en/Tutorials/images/bookstore-appservice-tests.png
diff --git a/docs/en/Tutorials/images/bookstore-book-list-2.png b/docs/en/Tutorials/images/bookstore-book-list-2.png
new file mode 100644
index 0000000000..a460d4241b
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-book-list-2.png differ
diff --git a/docs/en/Tutorials/images/bookstore-book-list.png b/docs/en/Tutorials/images/bookstore-book-list.png
new file mode 100644
index 0000000000..9e6cc9e010
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-book-list.png differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png b/docs/en/Tutorials/images/bookstore-books-table-actions.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png
rename to docs/en/Tutorials/images/bookstore-books-table-actions.png
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png b/docs/en/Tutorials/images/bookstore-books-table.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png
rename to docs/en/Tutorials/images/bookstore-books-table.png
diff --git a/docs/en/Tutorials/Angular/images/bookstore-confirmation-popup.png b/docs/en/Tutorials/images/bookstore-confirmation-popup.png
similarity index 100%
rename from docs/en/Tutorials/Angular/images/bookstore-confirmation-popup.png
rename to docs/en/Tutorials/images/bookstore-confirmation-popup.png
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png b/docs/en/Tutorials/images/bookstore-create-dialog-2.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png
rename to docs/en/Tutorials/images/bookstore-create-dialog-2.png
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png b/docs/en/Tutorials/images/bookstore-create-dialog.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png
rename to docs/en/Tutorials/images/bookstore-create-dialog.png
diff --git a/docs/en/Tutorials/images/bookstore-create-project-angular.png b/docs/en/Tutorials/images/bookstore-create-project-angular.png
new file mode 100644
index 0000000000..b9eb38b8b7
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-create-project-angular.png differ
diff --git a/docs/en/Tutorials/images/bookstore-create-project-mvc.png b/docs/en/Tutorials/images/bookstore-create-project-mvc.png
new file mode 100644
index 0000000000..f453b20279
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-create-project-mvc.png differ
diff --git a/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png b/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png
new file mode 100644
index 0000000000..6f19dcc7bf
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png differ
diff --git a/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png b/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png
new file mode 100644
index 0000000000..ec9ef4c42f
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png differ
diff --git a/docs/en/Tutorials/images/bookstore-database-tables-ef.png b/docs/en/Tutorials/images/bookstore-database-tables-ef.png
new file mode 100644
index 0000000000..857b10de5b
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-database-tables-ef.png differ
diff --git a/docs/en/Tutorials/images/bookstore-database-tables-mongodb.png b/docs/en/Tutorials/images/bookstore-database-tables-mongodb.png
new file mode 100644
index 0000000000..8d78bd9a54
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-database-tables-mongodb.png differ
diff --git a/docs/en/Tutorials/images/bookstore-edit-button.png b/docs/en/Tutorials/images/bookstore-edit-button.png
new file mode 100644
index 0000000000..bfc1c64797
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-edit-button.png differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-empty-new-book-modal.png b/docs/en/Tutorials/images/bookstore-empty-new-book-modal.png
similarity index 100%
rename from docs/en/Tutorials/Angular/images/bookstore-empty-new-book-modal.png
rename to docs/en/Tutorials/images/bookstore-empty-new-book-modal.png
diff --git a/docs/en/Tutorials/images/bookstore-final-actions-dropdown.png b/docs/en/Tutorials/images/bookstore-final-actions-dropdown.png
new file mode 100644
index 0000000000..4f41829f0d
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-final-actions-dropdown.png differ
diff --git a/docs/en/Tutorials/images/bookstore-generate-state-books.png b/docs/en/Tutorials/images/bookstore-generate-state-books.png
new file mode 100644
index 0000000000..be7a919017
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-generate-state-books.png differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png b/docs/en/Tutorials/images/bookstore-homepage.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png
rename to docs/en/Tutorials/images/bookstore-homepage.png
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png b/docs/en/Tutorials/images/bookstore-index-js-file-v2.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png
rename to docs/en/Tutorials/images/bookstore-index-js-file-v2.png
diff --git a/docs/en/Tutorials/images/bookstore-initial-book-list-page.png b/docs/en/Tutorials/images/bookstore-initial-book-list-page.png
new file mode 100644
index 0000000000..591cffb121
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-initial-book-list-page.png differ
diff --git a/docs/en/Tutorials/images/bookstore-initial-books-page-with-layout.png b/docs/en/Tutorials/images/bookstore-initial-books-page-with-layout.png
new file mode 100644
index 0000000000..629ad46444
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-initial-books-page-with-layout.png differ
diff --git a/docs/en/Tutorials/images/bookstore-localization-files-v2.png b/docs/en/Tutorials/images/bookstore-localization-files-v2.png
new file mode 100644
index 0000000000..542cda209c
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-localization-files-v2.png differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png b/docs/en/Tutorials/images/bookstore-menu-items.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png
rename to docs/en/Tutorials/images/bookstore-menu-items.png
diff --git a/docs/en/Tutorials/images/bookstore-migrations-applied-angular.png b/docs/en/Tutorials/images/bookstore-migrations-applied-angular.png
new file mode 100644
index 0000000000..0724e4ae8f
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-migrations-applied-angular.png differ
diff --git a/docs/en/Tutorials/images/bookstore-migrations-applied-mvc.png b/docs/en/Tutorials/images/bookstore-migrations-applied-mvc.png
new file mode 100644
index 0000000000..d59c0ce1d3
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-migrations-applied-mvc.png differ
diff --git a/docs/en/Tutorials/images/bookstore-new-book-button.png b/docs/en/Tutorials/images/bookstore-new-book-button.png
new file mode 100644
index 0000000000..8112fe1352
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-new-book-button.png differ
diff --git a/docs/en/Tutorials/Angular/images/bookstore-new-book-form-v2.png b/docs/en/Tutorials/images/bookstore-new-book-form-v2.png
similarity index 100%
rename from docs/en/Tutorials/Angular/images/bookstore-new-book-form-v2.png
rename to docs/en/Tutorials/images/bookstore-new-book-form-v2.png
diff --git a/docs/en/Tutorials/Angular/images/bookstore-new-book-form.png b/docs/en/Tutorials/images/bookstore-new-book-form.png
similarity index 100%
rename from docs/en/Tutorials/Angular/images/bookstore-new-book-form.png
rename to docs/en/Tutorials/images/bookstore-new-book-form.png
diff --git a/docs/en/Tutorials/images/bookstore-new-menu-item.png b/docs/en/Tutorials/images/bookstore-new-menu-item.png
new file mode 100644
index 0000000000..97bf7fc7c1
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-new-menu-item.png differ
diff --git a/docs/en/Tutorials/images/bookstore-open-package-manager-console.png b/docs/en/Tutorials/images/bookstore-open-package-manager-console.png
new file mode 100644
index 0000000000..a640eb2681
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-open-package-manager-console.png differ
diff --git a/docs/en/Tutorials/images/bookstore-pmc-add-book-migration-v2.png b/docs/en/Tutorials/images/bookstore-pmc-add-book-migration-v2.png
new file mode 100644
index 0000000000..2baea20236
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-pmc-add-book-migration-v2.png differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png b/docs/en/Tutorials/images/bookstore-pmc-add-book-migration.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png
rename to docs/en/Tutorials/images/bookstore-pmc-add-book-migration.png
diff --git a/docs/en/Tutorials/images/bookstore-service-terminal-output.png b/docs/en/Tutorials/images/bookstore-service-terminal-output.png
new file mode 100644
index 0000000000..cf6145e03f
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-service-terminal-output.png differ
diff --git a/docs/en/Tutorials/images/bookstore-solution-structure-angular.png b/docs/en/Tutorials/images/bookstore-solution-structure-angular.png
new file mode 100644
index 0000000000..07d064a880
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-solution-structure-angular.png differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution-v3.png b/docs/en/Tutorials/images/bookstore-solution-structure-mvc.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution-v3.png
rename to docs/en/Tutorials/images/bookstore-solution-structure-mvc.png
diff --git a/docs/en/Tutorials/images/bookstore-start-project-angular.png b/docs/en/Tutorials/images/bookstore-start-project-angular.png
new file mode 100644
index 0000000000..08abf845a8
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-start-project-angular.png differ
diff --git a/docs/en/Tutorials/images/bookstore-start-project-mvc.png b/docs/en/Tutorials/images/bookstore-start-project-mvc.png
new file mode 100644
index 0000000000..133dc6f131
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-start-project-mvc.png differ
diff --git a/docs/en/Tutorials/images/bookstore-swagger-book-dto-properties.png b/docs/en/Tutorials/images/bookstore-swagger-book-dto-properties.png
new file mode 100644
index 0000000000..66d630bb56
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-swagger-book-dto-properties.png differ
diff --git a/docs/en/Tutorials/images/bookstore-swagger.png b/docs/en/Tutorials/images/bookstore-swagger.png
new file mode 100644
index 0000000000..3ce36a11bc
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-swagger.png differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png b/docs/en/Tutorials/images/bookstore-test-js-proxy-getlist-network.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png
rename to docs/en/Tutorials/images/bookstore-test-js-proxy-getlist-network.png
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png b/docs/en/Tutorials/images/bookstore-test-js-proxy-getlist.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png
rename to docs/en/Tutorials/images/bookstore-test-js-proxy-getlist.png
diff --git a/docs/en/Tutorials/images/bookstore-test-projects-angular.png b/docs/en/Tutorials/images/bookstore-test-projects-angular.png
new file mode 100644
index 0000000000..6a8947238e
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-test-projects-angular.png differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png b/docs/en/Tutorials/images/bookstore-test-projects-mvc.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png
rename to docs/en/Tutorials/images/bookstore-test-projects-mvc.png
diff --git a/docs/en/Tutorials/images/bookstore-test-projects-v2.png b/docs/en/Tutorials/images/bookstore-test-projects-v2.png
new file mode 100644
index 0000000000..8701164d75
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-test-projects-v2.png differ
diff --git a/docs/en/Tutorials/images/bookstore-update-database-after-book-entity.png b/docs/en/Tutorials/images/bookstore-update-database-after-book-entity.png
new file mode 100644
index 0000000000..4889f4f757
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-update-database-after-book-entity.png differ
diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png b/docs/en/Tutorials/images/bookstore-user-management.png
similarity index 100%
rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png
rename to docs/en/Tutorials/images/bookstore-user-management.png
diff --git a/docs/en/Tutorials/images/bookstore-visual-studio-solution-v3.png b/docs/en/Tutorials/images/bookstore-visual-studio-solution-v3.png
new file mode 100644
index 0000000000..307e3516a5
Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-visual-studio-solution-v3.png differ
diff --git a/docs/en/Tutorials/images/mozilla-self-signed-cert-error.png b/docs/en/Tutorials/images/mozilla-self-signed-cert-error.png
new file mode 100644
index 0000000000..c9e2fc0e65
Binary files /dev/null and b/docs/en/Tutorials/images/mozilla-self-signed-cert-error.png differ
diff --git a/docs/en/Tutorials/part-3.md b/docs/en/Tutorials/part-3.md
new file mode 100644
index 0000000000..4cbefde69e
--- /dev/null
+++ b/docs/en/Tutorials/part-3.md
@@ -0,0 +1,198 @@
+## ASP.NET Core {{UI_Value}} Tutorial - Part 3
+````json
+//[doc-params]
+{
+ "UI": ["MVC","NG"]
+}
+````
+
+{{
+if UI == "MVC"
+ DB="ef"
+ DB_Text="Entity Framework Core"
+ UI_Text="mvc"
+else if UI == "NG"
+ DB="mongodb"
+ DB_Text="MongoDB"
+ UI_Text="angular"
+else
+ DB ="?"
+ UI_Text="?"
+end
+}}
+
+### About this tutorial
+
+This is the third part of the ASP.NET Core {{UI_Value}} tutorial series. See all parts:
+
+- [Part I: Creating the project and book list page](part-1.md)
+- [Part II: Creating, updating and deleting books](part-2.md)
+- **Part III: Integration tests (this tutorial)**
+
+*You can also check out [the video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by the community, based on this tutorial.*
+
+### Test projects in the solution
+
+This part covers the **server side** tests. There are several test projects in the solution:
+
+
+
+Each project is used to test the related project. Test projects use the following libraries for testing:
+
+* [Xunit](https://xunit.github.io/) as the main test framework.
+* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as the assertion library.
+* [NSubstitute](http://nsubstitute.github.io/) as the mocking library.
+
+### Adding test data
+
+Startup template contains the `BookStoreTestDataBuilder` class in the `Acme.BookStore.TestBase` project which creates initial data to run tests. Change the content of `BookStoreTestDataSeedContributor` class as show below:
+
+````csharp
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Data;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Domain.Repositories;
+using Volo.Abp.Guids;
+
+namespace Acme.BookStore
+{
+ public class BookStoreTestDataSeedContributor
+ : IDataSeedContributor, ITransientDependency
+ {
+ private readonly IRepository _bookRepository;
+ private readonly IGuidGenerator _guidGenerator;
+
+ public BookStoreTestDataSeedContributor(
+ IRepository bookRepository,
+ IGuidGenerator guidGenerator)
+ {
+ _bookRepository = bookRepository;
+ _guidGenerator = guidGenerator;
+ }
+
+ public async Task SeedAsync(DataSeedContext context)
+ {
+ await _bookRepository.InsertAsync(
+ new Book(id: _guidGenerator.Create(),
+ name: "Test book 1",
+ type: BookType.Fantastic,
+ publishDate: new DateTime(2015, 05, 24),
+ price: 21
+ )
+ );
+
+ await _bookRepository.InsertAsync(
+ new Book(id: _guidGenerator.Create(),
+ name: "Test book 2",
+ type: BookType.Science,
+ publishDate: new DateTime(2014, 02, 11),
+ price: 15
+ )
+ );
+ }
+ }
+}
+````
+
+* `IRepository` is injected and used it in the `SeedAsync` to create two book entities as the test data.
+
+### Testing the application service BookAppService
+* `IGuidGenerator` is injected to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases. Further information, see the [Guid generation document](https://docs.abp.io/{{Document_Language_Code}}/abp/{{Document_Version}}/Guid-Generation).
+
+Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project:
+
+````csharp
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Xunit;
+using Shouldly;
+using Volo.Abp.Application.Dtos;
+using Volo.Abp.Validation;
+using Microsoft.EntityFrameworkCore.Internal;
+
+namespace Acme.BookStore
+{
+ public class BookAppService_Tests : BookStoreApplicationTestBase
+ {
+ private readonly IBookAppService _bookAppService;
+
+ public BookAppService_Tests()
+ {
+ _bookAppService = 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 == "Test book 1");
+ }
+ }
+}
+````
+
+* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of users.
+
+Add a new test that creates a valid new book:
+
+````csharp
+[Fact]
+public async Task Should_Create_A_Valid_Book()
+{
+ //Act
+ var result = await _bookAppService.CreateAsync(
+ new CreateUpdateBookDto
+ {
+ 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");
+}
+````
+
+Add a new test that tries to create an invalid book and fails:
+
+````csharp
+[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(mem => mem == "Name"));
+}
+````
+
+* Since the `Name` is empty, ABP will throw an `AbpValidationException`.
+
+Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests:
+
+
+
+Congratulations, the green icons show, the tests have been successfully passed!
+
diff --git a/docs/en/_resources/Diagrams.docx b/docs/en/_resources/Diagrams.docx
new file mode 100644
index 0000000000..424e281470
Binary files /dev/null and b/docs/en/_resources/Diagrams.docx differ
diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json
index cbb4c80e72..46a3b54406 100644
--- a/docs/en/docs-nav.json
+++ b/docs/en/docs-nav.json
@@ -38,12 +38,16 @@
"text": "Application Development",
"items": [
{
- "text": "With ASP.NET Core MVC UI",
- "path": "Tutorials/AspNetCore-Mvc/Part-I.md"
+ "text": "Part-1: Creating a new solution and listing items",
+ "path": "Tutorials/Part-1.md"
},
{
- "text": "With Angular UI",
- "path": "Tutorials/Angular/Part-I.md"
+ "text": "Part-2: CRUD operations",
+ "path": "Tutorials/Part-2.md"
+ },
+ {
+ "text": "Part-3: Integration tests",
+ "path": "Tutorials/Part-3.md"
}
]
}
@@ -241,7 +245,7 @@
"items": [
{
"text": "API",
- "items": [
+ "items": [
{
"text": "Auto API Controllers",
"path": "AspNetCore/Auto-API-Controllers.md"
@@ -295,20 +299,26 @@
"path": "Entity-Framework-Core.md",
"items": [
{
- "text": "Switch to MySQL",
- "path": "Entity-Framework-Core-MySQL.md"
- },
- {
- "text": "Switch to PostgreSQL",
- "path": "Entity-Framework-Core-PostgreSQL.md"
- },
- {
- "text": "Switch to SQLite",
- "path": "Entity-Framework-Core-SQLite.md"
+ "text": "Database Migrations",
+ "path": "Entity-Framework-Core-Migrations.md"
},
{
- "text": "Switch to another DBMS",
- "path": "Entity-Framework-Core-Other-DBMS.md"
+ "text": "Switch DBMS",
+ "path": "Entity-Framework-Core-Other-DBMS.md",
+ "items": [
+ {
+ "text": "To MySQL",
+ "path": "Entity-Framework-Core-MySQL.md"
+ },
+ {
+ "text": "To PostgreSQL",
+ "path": "Entity-Framework-Core-PostgreSQL.md"
+ },
+ {
+ "text": "To SQLite",
+ "path": "Entity-Framework-Core-SQLite.md"
+ }
+ ]
}
]
},
@@ -338,6 +348,20 @@
{
"text": "RabbitMQ Integration",
"path": "Background-Jobs-RabbitMq.md"
+ },
+ {
+ "text": "Quartz Integration",
+ "path": "Background-Jobs-Quartz.md"
+ }
+ ]
+ },
+ {
+ "text": "Background Workers",
+ "path": "Background-Workers.md",
+ "items": [
+ {
+ "text": "Quartz Integration",
+ "path": "Background-Workers-Quartz.md"
}
]
}
@@ -386,4 +410,4 @@
"path": "Contribution/Index.md"
}
]
-}
\ No newline at end of file
+}
diff --git a/docs/en/docs-params.json b/docs/en/docs-params.json
new file mode 100644
index 0000000000..23d079f9bb
--- /dev/null
+++ b/docs/en/docs-params.json
@@ -0,0 +1,28 @@
+{
+ "parameters": [
+ {
+ "name": "UI",
+ "displayName": "UI",
+ "values": {
+ "MVC": "MVC / Razor Pages",
+ "NG": "Angular"
+ }
+ },
+ {
+ "name": "DB",
+ "displayName": "Database",
+ "values": {
+ "EF": "Entity Framework Core",
+ "Mongo": "MongoDB"
+ }
+ },
+ {
+ "name": "Tiered",
+ "displayName": "Tiered",
+ "values": {
+ "No": "Not Tiered",
+ "Yes": "Tiered"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/docs/en/images/bookstore-efcore-dependencies.png b/docs/en/images/bookstore-efcore-dependencies.png
new file mode 100644
index 0000000000..3de5a679ef
Binary files /dev/null and b/docs/en/images/bookstore-efcore-dependencies.png differ
diff --git a/docs/en/images/bookstore-second-database.png b/docs/en/images/bookstore-second-database.png
new file mode 100644
index 0000000000..3faf19496c
Binary files /dev/null and b/docs/en/images/bookstore-second-database.png differ
diff --git a/docs/en/images/multiple-database-usage.png b/docs/en/images/multiple-database-usage.png
new file mode 100644
index 0000000000..ada6177629
Binary files /dev/null and b/docs/en/images/multiple-database-usage.png differ
diff --git a/docs/en/images/pmc-add-migration-initial-update-database.png b/docs/en/images/pmc-add-migration-initial-update-database.png
new file mode 100644
index 0000000000..3cc45e5ddf
Binary files /dev/null and b/docs/en/images/pmc-add-migration-initial-update-database.png differ
diff --git a/docs/en/images/pmc-add-migration-role-title.png b/docs/en/images/pmc-add-migration-role-title.png
new file mode 100644
index 0000000000..69fc5b7bc5
Binary files /dev/null and b/docs/en/images/pmc-add-migration-role-title.png differ
diff --git a/docs/en/images/single-database-usage.png b/docs/en/images/single-database-usage.png
new file mode 100644
index 0000000000..108173fd86
Binary files /dev/null and b/docs/en/images/single-database-usage.png differ
diff --git a/docs/zh-Hans/Background-Jobs-Quartz.md b/docs/zh-Hans/Background-Jobs-Quartz.md
new file mode 100644
index 0000000000..906d0b2d1f
--- /dev/null
+++ b/docs/zh-Hans/Background-Jobs-Quartz.md
@@ -0,0 +1,73 @@
+# Quartz 后台作业管理
+
+[Quartz](https://www.quartz-scheduler.net/)是一个高级的作业管理. 你可以用ABP框架集成Quartz代替[默认后台作业管理](Background-Jobs.md). 通过这种方式你可以使用相同的后台作业API,将你的代码独立于Quartz. 如果你喜欢也可以直接使用Quartz的API.
+
+> 参阅[后台作业文档](Background-Jobs.md),学习如何使用后台作业系统. 本文只介绍了如何安装和配置Quartz集成.
+
+## 安装
+
+建议使用[ABP CLI](CLI.md)安装包.
+
+### 使用ABP CLI
+
+在项目的文件夹(.csproj文件)中打开命令行窗口输入以下命令:
+
+````bash
+abp add-package Volo.Abp.BackgroundJobs.Quartz
+````
+
+### 手动安装
+
+如果你想手动安装;
+
+1. 添加 [Volo.Abp.BackgroundJobs.Quartz](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.Quartz) NuGet包添加到你的项目:
+
+ ````
+ Install-Package Volo.Abp.BackgroundJobs.Quartz
+ ````
+
+2. 添加 `AbpBackgroundJobsQuartzModule` 到你的模块的依赖列表:
+
+````csharp
+[DependsOn(
+ //...other dependencies
+ typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency
+ )]
+public class YourModule : AbpModule
+{
+}
+````
+
+## 配置
+
+Quartz是一个可配置的类库,对此ABP框架提供了 `AbpQuartzPreOptions`. 你可以在模块预配置此选项,ABP在初始化Quartz模块时将使用它. 例:
+
+````csharp
+[DependsOn(
+ //...other dependencies
+ typeof(AbpBackgroundJobsQuartzModule) //Add the new module dependency
+ )]
+public class YourModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ var configuration = context.Services.GetConfiguration();
+
+ PreConfigure(options =>
+ {
+ options.Properties = new NameValueCollection
+ {
+ ["quartz.jobStore.dataSource"] = "BackgroundJobsDemoApp",
+ ["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
+ ["quartz.jobStore.tablePrefix"] = "QRTZ_",
+ ["quartz.serializer.type"] = "json",
+ ["quartz.dataSource.BackgroundJobsDemoApp.connectionString"] = configuration.GetConnectionString("Quartz"),
+ ["quartz.dataSource.BackgroundJobsDemoApp.provider"] = "SqlServer",
+ ["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz",
+ };
+ });
+ }
+}
+````
+
+Quartz**默认**将作业与调度信息存储在**内存**中,示例中我们使用[选项模式](Options.md)的预配置将其更改为存储到数据库中. 有关Quartz的更多配置请参阅[Quartz文档](https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/index.html).
\ No newline at end of file
diff --git a/docs/zh-Hans/Background-Jobs.md b/docs/zh-Hans/Background-Jobs.md
index 7c2a694005..398a8efc8b 100644
--- a/docs/zh-Hans/Background-Jobs.md
+++ b/docs/zh-Hans/Background-Jobs.md
@@ -7,13 +7,13 @@
- 为执行**长时间运行的任务**而用户无需等待, 例如:用户按了一下"报告"按钮开始一个长时间运行的报告任务, 你把这个任务添加到**队列**里,并在完成后通过电子邮件将报告的结果发送给你的用户.
- 创建**可重试**和**持久的任务**以**确保**代码将**成功执行**. 例如, 你可以在后台作业中发送电子邮件以克服**临时故障**并**保证**最终发送. 这样用户不需要在发送电子邮件时等待.
-后台作业是**持久性的**这意味着即使你的应用程序崩溃了, 后台左右也会在稍后**重试**并**执行**.
+后台作业是**持久性的**这意味着即使你的应用程序崩溃了, 后台作业也会在稍后**重试**并**执行**.
ABP为后台作业提供了一个**抽象**模块和几个后台作业**实现**. 它具有内置/默认的实现以及与Hangfire和RabbitMQ的集成.
## 抽象模块
-ABP为后台作业提供了一个 **abstraction** 模块和 **多个实现**. 它有一个内置/默认实现以及Hangfire与RabbitMQ集成.
+ABP为后台作业提供了一个 **抽象** 模块和 **多个实现**. 它有一个内置/默认实现以及Hangfire,RabbitMQ与Quartz集成.
`Volo.Abp.BackgroundJobs.Abstractions` nuget package 提供了创建后台作业和队列作业所需要的服务. 如果你的模块只依赖这个包,那么它可以独立于其实现/集成.
@@ -174,4 +174,5 @@ public class MyModule : AbpModule
请参阅预构建的作业管理器备选方案:
* [Hangfire 后台作业管理器](Background-Jobs-Hangfire.md)
-* [RabbitMQ 后台作业管理器](Background-Jobs-RabbitMq.md)
\ No newline at end of file
+* [RabbitMQ 后台作业管理器](Background-Jobs-RabbitMq.md)
+* [Quartz 后台作业管理器](Background-Jobs-Quartz.md)
\ No newline at end of file
diff --git a/docs/zh-Hans/Background-Workers-Quartz.md b/docs/zh-Hans/Background-Workers-Quartz.md
new file mode 100644
index 0000000000..4799633b07
--- /dev/null
+++ b/docs/zh-Hans/Background-Workers-Quartz.md
@@ -0,0 +1,68 @@
+# Quartz 后台工作者管理
+
+[Quartz](https://www.quartz-scheduler.net/)是一个高级的后台工作者管理. 你可以用ABP框架集成Quartz代替[默认后台工作者管理](Background-Workers.md). ABP简单的集成了Quartz.
+
+## 安装
+
+建议使用[ABP CLI](CLI.md)安装包.
+
+### 使用ABP CLI
+
+在项目的文件夹(.csproj文件)中打开命令行窗口输入以下命令:
+
+````bash
+abp add-package Volo.Abp.BackgroundWorkers.Quartz
+````
+
+### 手动安装
+
+如果你想手动安装;
+
+1. 添加 [Volo.Abp.BackgroundWorkers.Quartz](https://www.nuget.org/packages/Volo.Abp.BackgroundWorkers.Quartz) NuGet包添加到你的项目:
+
+ ````
+ Install-Package Volo.Abp.BackgroundWorkers.Quartz
+ ````
+
+2. 添加 `AbpBackgroundWorkersQuartzModule` 到你的模块的依赖列表:
+
+````csharp
+[DependsOn(
+ //...other dependencies
+ typeof(AbpBackgroundWorkersQuartzModule) //Add the new module dependency
+ )]
+public class YourModule : AbpModule
+{
+}
+````
+
+### 配置
+
+参阅[配置](Background-Jobs-Quartz.md#配置).
+
+### 创建后台工作者
+
+后台工作者是一个继承自 `QuartzBackgroundWorkerBase` 基类的类. 一个简单的工作者如下所示:
+
+```` csharp
+public class MyLogWorker : QuartzBackgroundWorkerBase
+{
+ public MyLogWorker()
+ {
+ JobDetail = JobBuilder.Create().Build();
+ Trigger = TriggerBuilder.Create().StartNow().Build();
+ }
+
+ public override Task Execute(IJobExecutionContext context)
+ {
+ Logger.LogInformation("Executed MyLogWorker..!");
+ return Task.CompletedTask;
+ }
+}
+````
+
+示例中我们重写了 `Execute` 方法写入日志. 后台工作者默认是**单例**. 如果你需要,也可以实现[依赖接口](Dependency-Injection.md#依赖接口)将其注册为其他的生命周期.
+
+### 更多
+
+参阅Quartz[文档](https://www.quartz-scheduler.net/documentation/index.html)了解更多信息.
\ No newline at end of file
diff --git a/docs/zh-Hans/Background-Workers.md b/docs/zh-Hans/Background-Workers.md
new file mode 100644
index 0000000000..675ca882ec
--- /dev/null
+++ b/docs/zh-Hans/Background-Workers.md
@@ -0,0 +1,3 @@
+# 后台工作者
+
+TODO
\ No newline at end of file
diff --git a/docs/zh-Hans/CLI.md b/docs/zh-Hans/CLI.md
index f5f230d9f2..7ace4bab5f 100644
--- a/docs/zh-Hans/CLI.md
+++ b/docs/zh-Hans/CLI.md
@@ -43,7 +43,7 @@ abp new Acme.BookStore
* `app` (default): [应用程序模板](Startup-Templates/Application.md). 其他选项:
* `--ui` 或者 `-u`: 指定ui框架.默认`mvc`框架.其他选项:
* `mvc`: ASP.NET Core MVC.此模板的其他选项:
- * `--tiered`: 创建分层解决方案,Web和Http Api层在物理上是分开的.如果未指定会创建一个分层的解决方案,此解决方案没有那么复杂,适合大多数场景.
+ * `--tiered`: 创建分层解决方案,Web和Http Api层在物理上是分开的.如果未指定会创建一个分层的解决方案,此解决方案没有那么复杂,适合大多数场景.
* `angular`: Angular. 这个模板还有一些额外的选项:
* `--separate-identity-server`: 将Identity Server应用程序与API host应用程序分开. 如果未指定,则服务器端将只有一个端点.
* `none`: 无UI. 这个模板还有一些额外的选项:
@@ -53,8 +53,9 @@ abp new Acme.BookStore
* `mongodb`: MongoDB.
* `module`: [Module template](Startup-Templates/Module.md). 其他选项:
* `--no-ui`: 不包含UI.仅创建服务模块(也称为微服务 - 没有UI).
-* `--output-folder` 或者 `-o`: 指定输出文件夹,默认是当前目录.
-* `--version` 或者 `-v`: 指定ABP和模板的版本.它可以是 [release tag](https://github.com/abpframework/abp/releases) 或者 [branch name](https://github.com/abpframework/abp/branches). 如果没有指定,则使用最新版本.大多数情况下,您会希望使用最新的版本.
+* `--output-folder` 或者 `-o`: 指定输出文件夹,默认是当前目录.
+* `--version` 或者 `-v`: 指定ABP和模板的版本.它可以是 [release tag](https://github.com/abpframework/abp/releases) 或者 [branch name](https://github.com/abpframework/abp/branches). 如果没有指定,则使用最新版本.大多数情况下,您会希望使用最新的版本.
+* `--template-source` 或者 `-ts`: 指定自定义模板源用于生成项目,可以使用本地源和网络源(例如 `D\localTemplate` 或 `https://.zip`).
### add-package
@@ -125,6 +126,46 @@ abp update [options]
#### Options
* `--include-previews` 或 `-p`: 将预览版, 测试版本 和 rc 包 同时更新到最新版本.
+* `--npm`: 仅更新NPM包
+* `--nuget`: 仅更新的NuGet包
+
+### 切换到每晚构建(预览)包
+
+想要切换到ABP框架的最新预览版可以使用此命令.
+
+用法:
+
+````bash
+abp switch-to-preview [options]
+````
+
+你也可以使用切换回稳定版本:
+
+````bash
+abp switch-to-stable [options]
+````
+
+#### Options
+
+`--solution-path` 或 `-sp`: 指定解决方案(.sln)文件路径. 如果未指定,CLI试寻找当前目录中的.sln文件.
+
+### login
+
+CLI的一些功能需要登录到abp.io平台. 使用你的用户名登录
+
+```bash
+abp login
+```
+
+请注意,新的登录将终止先前的会话并创建一个新的会话.
+
+### logout
+
+通过从计算机中删除会话令牌来注销.
+
+```
+abp logout
+```
### help
diff --git a/docs/zh-Hans/Entities.md b/docs/zh-Hans/Entities.md
index 9c3066fef4..66ba94a0cd 100644
--- a/docs/zh-Hans/Entities.md
+++ b/docs/zh-Hans/Entities.md
@@ -1,34 +1,80 @@
-## 实体
+# 实体
实体是DDD(Domain Driven Design)中核心概念.Eric Evans是这样描述实体的 "一个没有从其属性,而是通过连续性和身份的线索来定义的对象"
实体通常映射到关系型数据库的表中.
-### 实体类
+## 实体类
实体都继承自`Entity`类,如下所示:
```C#
-public class Person : Entity
+public class Book : Entity
{
public string Name { get; set; }
- public DateTime CreationTime { get; set; }
+ public float Price { get; set; }
+}
+```
+
+> 如果你不想继承基类`Entity`,也可以直接实现`IEntity`接口
+
+`Entity`类只是用给定的主 **键类型** 定义了一个`Id`属性,在上面的示例中是`Guid`类型.可以是其他类型如`string`, `int`, `long`或其他你需要的类型.
+
+### Guid主键的实体
+
+如果你的实体Id类型为 `Guid`,有一些好的实践可以实现:
- public Person()
+* 创建一个构造函数,获取ID作为参数传递给基类.
+ * 如果没有为GUID Id斌值,ABP框架会在保存时设置它,但是在将实体保存到数据库之前最好在实体上有一个有效的Id.
+* 如果使用带参数的构造函数创建实体,那么还要创建一个 `protected` 构造函数. 当数据库提供程序从数据库读取你的实体时(反序列化时)将使用它.
+* 不要使用 `Guid.NewGuid()` 来设置Id! 在创建实体的代码中使用[`IGuidGenerator`服务](Guid-Generation.md)传递Id参数. `IGuidGenerator`经过优化可以产生连续的GUID.这对于关系数据库中的聚集索引非常重要.
+
+示例实体:
+
+````csharp
+public class Book : Entity
+{
+ public string Name { get; set; }
+ public float Price { get; set; }
+ protected Book()
+ {
+ }
+ public Book(Guid id)
+ : base(id)
{
- CreationTime = DateTime.Now;
}
}
-```
+````
-> 如果你不想继承基类`Entity`,也可以直接实现`IEntity`接口
+在[应用服务](Application-Services.md)中使用示例:
-`Entity`类只是用给定的主 **键类型** 定义了一个`Id`属性,在上面的示例中是`int`类型.可以是其他类型如`string`, `Guid`, `long`或其他你需要的类型.
+````csharp
+public class BookAppService : ApplicationService, IBookAppService
+{
+ private readonly IRepository _bookRepository;
+ public BookAppService(IRepository bookRepository)
+ {
+ _bookRepository = bookRepository;
+ }
+ public async Task CreateAsync(CreateBookDto input)
+ {
+ await _bookRepository.InsertAsync(
+ new Book(GuidGenerator.Create())
+ {
+ Name = input.Name,
+ Price = input.Price
+ }
+ );
+ }
+}
+````
-实体类还重写了 **equality** 运算符(==),以方便地检查两个实体是否相等(如果它们是相同的类型并且它们的Id相等,则它们是相等的).
+* `BookAppService` 注入图书实体的默认[仓库](Repositories.md),使用`InsertAsync`方法插入 `Book` 到数据库中.
+* `GuidGenerator`类型是 `IGuidGenerator`,它是在`ApplicationService`基类中定义的属性. ABP将这样常用属性预注入,所以不需要手动[注入](Dependency-Injection.md).
+* 如果您想遵循DDD最佳实践,请参阅下面的*聚合示例*部分.
-#### 具有复合键的实体
+### 具有复合键的实体
有些实体可能需要 **复合键** .在这种情况下,可以从非泛型`Entity`类派生实体.如:
@@ -53,30 +99,30 @@ public class UserRole : Entity
}
````
-上面的例子中,复合键由`UserId`和`RoleId`组成.在关系数据库中,它是相关表的复合主键.
+上面的例子中,复合键由`UserId`和`RoleId`组成.在关系数据库中,它是相关表的复合主键. 具有复合键的实体应当实现上面代码中所示的`GetKeys()`方法.
-具有复合键的实体应当实现上面代码中所示的`GetKeys()`方法.
+> 你还需要在**对象关系映射**(ORM)中配置实体的键. 参阅[Entity Framework Core](Entity-Framework-Core.md)集成文档查看示例.
-你还需要在 **object-to-relational mapping**(ORM)中配置实体的键.
+> 需要注意,复合主键实体不可以使用 `IRepository` 接口,因为它需要一个唯一的Id属性. 但你可以使用 `IRepository`.更多信息请参见[仓储](Repositories.md)的文档.
-> 复合主键在仓储中有限制.由于不知道Id属性,所以对于这些实体,不能使用`IRepository`.但是,可以使用`IRepository`.更多信息请参见[仓储](Repositories.md)的文档.
-
-### 聚合根
+## 聚合根
"*聚合是域驱动设计中的一种模式.DDD的聚合是一组可以作为一个单元处理的域对象.例如,订单及订单系列的商品,这些是独立的对象,但将订单(连同订单系列的商品)视为一个聚合通常是很有用的*"( [查看详细介绍](http://martinfowler.com/bliki/DDD_Aggregate.html))
-`AggregateRoot`类继承自`Entity`类,所以默认有`Id`这个属性
+`AggregateRoot`类继承自`Entity`类,所以默认有`Id`这个属性
> 值得注意的是 ABP 会默认为聚合根创建仓储,当然,ABP也可以为所有的实体创建仓储,详情参见[仓储](Repositories.md).
-ABP不强制你使用聚合根,实际上你可以使用上面定义的`Entity`类,当然,如果你想实现DDD并且创建聚合根,这里有一些最佳实践仅供参考:
+ABP不强制你使用聚合根,实际上你可以使用上面定义的`Entity`类,当然,如果你想实现[领域驱动设计](Domain-Driven-Design.md)并且创建聚合根,这里有一些最佳实践仅供参考:
* 聚合根需要维护自身的完整性,所有的实体也是这样.但是聚合根也要维护子实体的完整性.所以,聚合根必须一直有效.
* 使用Id引用聚合根,而不使用导航属性
* 聚合根被视为一个单元.它是作为一个单元检索和更新的.它通常被认为是一个交易边界.
* 不单独修改聚合根中的子实体
-#### 聚合根例子
+如果你想在应用程序中实现DDD,请参阅[实体设计最佳实践指南](Best-Practices/Entities.md).
+
+### 聚合根例子
这是一个具有子实体集合的聚合根例子:
@@ -155,6 +201,11 @@ public class OrderLine : Entity
{
Count = newCount;
}
+
+ public override object[] GetKeys()
+ {
+ return new Object[] {OrderId, ProductId};
+ }
}
````
@@ -169,8 +220,73 @@ public class OrderLine : Entity
* `Order.AddProduct`实现了业务规则将商品添加到订单中
* 所有属性都有`protected`的set.这是为了防止实体在实体外部任意改变.因此,在没有向订单中添加新产品的情况下设置 `TotalItemCount`将是危险的.它的值由`AddProduct`方法维护.
-ABP不强制你应用任何DDD规则或模式.但是,当你准备应用的DDD规则或模式时候,ABP会让这变的可能而且更简单.文档同样遵循这个原则.
+ABP框架不强制你应用任何DDD规则或模式.但是,当你准备应用的DDD规则或模式时候,ABP会让这变的可能而且更简单.文档同样遵循这个原则.
-#### 带有组合键的聚合根
+### 带有组合键的聚合根
虽然这种聚合根并不常见(也不建议使用),但实际上可以按照与上面提到的跟实体相同的方式定义复合键.在这种情况下,要使用非泛型的`AggregateRoot`基类.
+
+## 基类和接口的审计属性
+
+有一些属性,像`CreationTime`,`CreatorId`,`LastModificationTime`...在所有应用中都很常见. ABP框架提供了一些接口和基类来**标准化**这些属性,并**自动设置它们的值**.
+
+### 审计接口
+
+有很多的审计接口,你可以实现一个你需要的那个.
+
+> 虽然可以手动实现这些接口,但是可以使用下一节中定义的**基类**简化代码.
+
+* `IHasCreationTime` 定义了以下属性:
+ * `CreationTime`
+* `IMayHaveCreator` 定义了以下属性:
+ * `CreatorId`
+* `ICreationAuditedObject` 继承 `IHasCreationTime` 和 `IMayHaveCreator`, 所以它定义了以下属性:
+ * `CreationTime`
+ * `CreatorId`
+* `IHasModificationTime` 定义了以下属性:
+ * `LastModificationTime`
+* `IModificationAuditedObject` 扩展 `IHasModificationTime` 并添加了 `LastModifierId` 属性. 所以它定义了以下属性:
+ * `LastModificationTime`
+ * `LastModifierId`
+* `IAuditedObject` 扩展 `ICreationAuditedObject` 和 `IModificationAuditedObject`, 所以它定义了以下属性:
+ * `CreationTime`
+ * `CreatorId`
+ * `LastModificationTime`
+ * `LastModifierId`
+* `ISoftDelete` (参阅 [数据过滤文档](Data-Filtering.md)) 定义了以下属性:
+ * `IsDeleted`
+* `IHasDeletionTime` 扩展 `ISoftDelete` 并添加了 `DeletionTime` 属性. 所以它定义了以下属性:
+ * `IsDeleted`
+ * `DeletionTime`
+* `IDeletionAuditedObject` 扩展 `IHasDeletionTime` 并添加了 `DeleterId` 属性. 所以它定义了以下属性:
+ * `IsDeleted`
+ * `DeletionTime`
+ * `DeleterId`
+* `IFullAuditedObject` 继承 `IAuditedObject` 和 `IDeletionAuditedObject`, 所以它定义了以下属性:
+ * `CreationTime`
+ * `CreatorId`
+ * `LastModificationTime`
+ * `LastModifierId`
+ * `IsDeleted`
+ * `DeletionTime`
+ * `DeleterId`
+
+当你实现了任意接口,或者从下一节定义的类派生,ABP框架就会尽可能地自动管理这些属性.
+
+> 实现 `ISoftDelete` , `IDeletionAuditedObject` 或 `IFullAuditedObject` 让你的实体**软删除**. 参阅[数据过滤文档](Data-Filtering.md),了解软删除模式.
+
+### 审计基类
+
+虽然可以手动实现以上定义的任何接口,但建议从这里定义的基类继承:
+
+* `CreationAuditedEntity` 和 `CreationAuditedAggregateRoot` 实现了 `ICreationAuditedObject` 接口.
+* `AuditedEntity` 和 `AuditedAggregateRoot` 实现了 `IAuditedObject` 接口.
+* `FullAuditedEntity` and `FullAuditedAggregateRoot` 实现了 `IFullAuditedObject` 接口.
+
+所有这些基类都有非泛型版本,可以使用 `AuditedEntity` 和 `FullAuditedAggregateRoot` 来支持复合主键;
+
+所有这些基类也有 `... WithUser`,像 `FullAuditedAggregateRootWithUser` 和 `FullAuditedAggregateRootWithUser`. 这样就可以将导航属性添加到你的用户实体. 但在聚合根之间添加导航属性不是一个好做法,所以这种用法是不建议的(除非你使用EF Core之类的ORM可以很好地支持这种情况,并且你真的需要它. 请记住这种方法不适用于NoSQL数据库(如MongoDB),你必须真正实现聚合模式).
+
+## 另请参阅
+
+* [实体设计最佳实践指南](Best-Practices/Entities.md)
\ No newline at end of file
diff --git a/docs/zh-Hans/Entity-Framework-Core-Migrations.md b/docs/zh-Hans/Entity-Framework-Core-Migrations.md
new file mode 100644
index 0000000000..6cf8f281e3
--- /dev/null
+++ b/docs/zh-Hans/Entity-Framework-Core-Migrations.md
@@ -0,0 +1,930 @@
+
+# EF Core数据库迁移
+
+本文首先介绍[应用程序启动模板](Startup-Templates/Application.md)提供的**默认结构**,并讨论您可能希望为自己的应用程序实现的**各种场景**.
+
+> 本文档适用于希望完全理解和自定义[应用程序启动模板](Startup-Templates/Application.md)附带的数据库结构的人员. 如果你只是想创建实体和管理代码优先(code first)迁移,只需要遵循[启动教程](Tutorials/Index.md).
+
+### 源码
+
+你可以在[这里](https://github.com/abpframework/abp/tree/dev/samples/EfCoreMigrationDemo)找到本文引用的示例项目的源代码; 但是为了理解示例项目的源代码,你需要阅读和理解这个文档.
+
+## 关于EF Core 代码优先迁移
+
+Entity Framework Core 提供了一种简单强大[数据库迁移系统](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). ABP框架[启动模板](Startup-Templates/Index.md)使用这个系统,让你以标准的方式开发你的应用程序.
+
+但是EF Core迁移系统在**模块化环境中不是很好**,在模块化环境中,每个模块都维护**自己的数据库模式**,而实际上两个或多个模块可以**共享一个数据库**.
+
+由于ABP框架在所有方面都关心模块化,所以它为这个问题提供了**解决方案**. 如果你需要**自定义数据库结构**,那么应当了解这个解决方案.
+
+> 参阅[EF Core文档](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)充分了解EF Core Code First迁移,以及为什么需要这样的系统.
+
+## 默认解决方案与数据库配置
+
+当你[创建一个新的Web应用程序](https://abp.io/get-started)(使用EF Core,它是默认的数据库提供程序),你的解决方案结构类似下图:
+
+
+
+实际的解决方案结构可能会根据你的偏好有所不同,但是数据库部分是相同的.
+
+> 本文档将使用 `Acme.BookStore` 示例项目名称来引用项目和类. 你需要在解决方案中找到相应的类/项目.
+
+### 数据库模式
+
+启动模板已预安装了一些[应用程序模块](Modules/Index.md). 解决方案的每一层都有相应的模块包引用. 所以 `.EntityFrameworkCore` 项目含有使用 `EntityFrameworkCore` 模块的Nuget的引用:
+
+
+
+通过这种方式,你可以看到所有的 `.EntityFrameworkCore` 项目下的**EF Core的依赖**.
+
+> 除了模块引用之外,它还引用了 `Volo.Abp.EntityFrameworkCore.SqlServer` 包,因为启动模板预配置的是Sql Server. 参阅文档了解如何[切换到其它DBMS](Entity-Framework-Core-Other-DBMS.md).
+
+虽然每个模块在设计上有自己的 `DbContext` 类,并且可以使用其自己的**物理数据库**,但解决方案的配置是使用**单个共享数据库**如下图所示:
+
+
+
+这是**最简单的配置**,适用于大部分的应用程序. `appsettings.json` 文件有名为`Default`**单个连接字符串**:
+
+````json
+"ConnectionStrings": {
+ "Default": "..."
+}
+````
+
+所以你有一个**单一的数据库模式**,其中包含**共享**此数据库的模块的所有表.
+
+ABP框架的[连接字符串](Connection-Strings.md)系统允许你轻松为所需的模块**设置不同的连接字符串**:
+
+````json
+"ConnectionStrings": {
+ "Default": "...",
+ "AbpAuditLogging": "..."
+}
+````
+
+示例配置告诉ABP框架[审计日志模块](Modules/Audit-Logging.md)应使用第二个连接字符串.
+
+**然而这仅仅只是开始**. 你还需要创建第二个数据库以及里面审计日志表并使用code frist迁移的方法维护数据库表. 本文档的主要目的之一就是指导你了解这样的数据库分离场景.
+
+#### 模块表
+
+每个模块都使用**自己的数据库表**. 例如[身份模块](Modules/Identity.md)有一些表来管理系统中的用户和角色.
+
+##### 表前缀
+
+由于所有模块都允许共享一个数据库(这是默认配置),所以模块通常使用**表前缀**来对自己的表进行分组.
+
+基础模块(如[身份](Modules/Identity.md), [租户管理](Modules/Tenant-Management.md) 和 [审计日志](Modules/Audit-Logging.md))使用 `Abp` 前缀, 其他的模块使用自己的前缀. 如[Identity Server](Modules/IdentityServer.md) 模块使用前缀 `IdentityServer`.
+
+如果你愿意,你可以为你的应用程序的模块**更改数据库表前缀**.
+例:
+
+````csharp
+Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids";
+````
+
+这段代码更改了[Identity Server](Modules/IdentityServer.md)的前缀. 在应用程序的**最开始**编写这段代码.
+
+> 每个模块还定义了 `DbSchema` 属性,你可以在支持schema的数据库中使用它.
+
+### 项目
+
+从数据库的角度来看.有三个重要的项目将在下一节中解释.
+
+#### .EntityFrameworkCore 项目
+
+这个项目有应用程序的 `DbContext`类(本例中的 `BookStoreDbContex` ).
+
+**每个模块都使用自己的 `DbContext` 类**来访问数据库。同样你的应用程序有它自己的 `DbContext`. 通常在应用程序中使用这个 `DbContet`(如果你遵循最佳实践,应该在自定义[仓储](Repositories.md)中使用). 它几乎是一个空的 `DbContext`,因为你的应用程序在一开始没有任何实体,除了预定义的 `AppUser` 实体:
+
+````csharp
+[ConnectionStringName("Default")]
+public class BookStoreDbContext : AbpDbContext
+{
+ public DbSet Users { get; set; }
+
+ /* Add DbSet properties for your Aggregate Roots / Entities here. */
+
+ public BookStoreDbContext(DbContextOptions options)
+ : base(options)
+ {
+
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+
+ /* Configure the shared tables (with included modules) here */
+
+ builder.Entity(b =>
+ {
+ //Sharing the same table "AbpUsers" with the IdentityUser
+ b.ToTable("AbpUsers");
+
+ //Configure base properties
+ b.ConfigureByConvention();
+ b.ConfigureAbpUser();
+
+ //Moved customization of the "AbpUsers" table to an extension method
+ b.ConfigureCustomUserProperties();
+ });
+
+ /* Configure your own tables/entities inside the ConfigureBookStore method */
+ builder.ConfigureBookStore();
+ }
+}
+````
+
+这个简单的 `DbContext` 类仍然需要一些解释:
+
+* 它定义了一个 `[connectionStringName]` Attribute,它告诉ABP始终为此 `Dbcontext` 使用 `Default` 连接字符串.
+* 它从 `AbpDbContext` 而不是标准的 `DbContext` 类继承. 你可以参阅[EF Core集成](Entity-Framework-Core.md)文档了解更多. 现在你需要知道 `AbpDbContext` 基类实现ABP框架的一些约定,为你自动化一些常见的任务.
+* 它为 `AppUser` 实体定义了 `DbSet` 属性. `AppUser` 与[身份模块](Modules/Identity.md)的 `IdentityUser` 实体共享同一个表(默认名为 `AbpUsers`). 启动模板在应用程序中提供这个实体,因为我们认为用户实体一般需要应用程序中进行定制.
+* 构造函数接受一个 `DbContextOptions` 实例.
+* 它覆盖了 `OnModelCreating` 方法定义EF Core 映射.
+ * 首先调用 `base.OnModelCreating` 方法让ABP框架为我们实现基础映射.
+ * 然后它配置了 `AppUser` 实体的映射. 这个实体有一个特殊的情况(它与Identity模块共享一个表),在下一节中进行解释.
+ * 最后它调用 `builder.ConfigureBookStore()` 扩展方法来配置应用程序的其他实体.
+
+在介绍其他数据库相关项目之后,将更详细地说明这个设计.
+
+#### .EntityFrameworkCore.DbMigrations 项目
+
+正如前面所提到的,每个模块(和你的应用程序)有**它们自己**独立的 `DbContext` 类. 每个 `DbContext` 类只定义了自身模块的实体到表的映射,每个模块(包括你的应用程序)在**运行时**都使用相关的 `DbContext` 类.
+
+如你所知,EF Core Code First迁移系统依赖于 `DbContext` 类来跟踪和生成Code First迁移. 那么我们应该使用哪个 `DbContext` 进行迁移? 答案是它们都不是. `.EntityFrameworkCore.DbMigrations` 项目中定义了另一个 `DbContext` (示例解决方案中的 `BookStoreMigrationsDbContext`).
+
+##### MigrationsDbContext
+
+`MigrationsDbContext` 仅用于创建和应用数据库迁移. **不在运行时使用**. 它将所有使用的模块的所有实体到表的映射以及应用程序的映射**合并**.
+
+通过这种方式你可以创建和维护**单个数据库迁移路径**. 然而这种方法有一些困难,接下来的章节将解释ABP框架如何克服这些困难. 首先以 `BookStoreMigrationsDbContext` 类为例:
+
+````csharp
+/* This DbContext is only used for database migrations.
+ * It is not used on runtime. See BookStoreDbContext for the runtime DbContext.
+ * It is a unified model that includes configuration for
+ * all used modules and your application.
+ */
+public class BookStoreMigrationsDbContext : AbpDbContext
+{
+ public BookStoreMigrationsDbContext(
+ DbContextOptions options)
+ : base(options)
+ {
+
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+
+ /* Include modules to your migration db context */
+ builder.ConfigurePermissionManagement();
+ builder.ConfigureSettingManagement();
+ builder.ConfigureBackgroundJobs();
+ builder.ConfigureAuditLogging();
+ builder.ConfigureIdentity();
+ builder.ConfigureIdentityServer();
+ builder.ConfigureFeatureManagement();
+ builder.ConfigureTenantManagement();
+
+ /* Configure customizations for entities from the modules included */
+ builder.Entity(b =>
+ {
+ b.ConfigureCustomUserProperties();
+ });
+
+ /* Configure your own tables/entities inside the ConfigureBookStore method */
+ builder.ConfigureBookStore();
+ }
+}
+````
+
+##### 共享映射代码
+
+第一个问题是: 一个模块使用自己的 `DbContext` 这就需要到数据库的映射. 该 `MigrationsDbContext` 也需要相同的映射创建此模块的数据库表. 我们绝对**不希望复制**的映射代码.
+
+解决方案是定义一个**扩展方法**(在`ModelBuilder`)由两个 `DbContext` 类调用. 所以每个模块都定义了这样的扩展方法.
+
+例如 `builder.ConfigureBackgroundJobs()` 方法调用[后台作业模块](Modules/Background-Jobs.md)配置数据库表. 扩展方法的定义如下:
+
+````csharp
+public static class BackgroundJobsDbContextModelCreatingExtensions
+{
+ public static void ConfigureBackgroundJobs(
+ this ModelBuilder builder,
+ Action optionsAction = null)
+ {
+ var options = new BackgroundJobsModelBuilderConfigurationOptions(
+ BackgroundJobsDbProperties.DbTablePrefix,
+ BackgroundJobsDbProperties.DbSchema
+ );
+
+ optionsAction?.Invoke(options);
+
+ builder.Entity(b =>
+ {
+ b.ToTable(options.TablePrefix + "BackgroundJobs", options.Schema);
+
+ b.ConfigureCreationTime();
+ b.ConfigureExtraProperties();
+
+ b.Property(x => x.JobName)
+ .IsRequired()
+ .HasMaxLength(BackgroundJobRecordConsts.MaxJobNameLength);
+
+ //...
+ });
+ }
+}
+````
+
+此u还获取选项用于更改此模块的数据库表前缀和模式,但在这里并不重要.
+
+最终的应用程序在 `MigrationsDbContext` 类中调用扩展方法, 因此它可以确定此 `MigrationsDbContext` 维护的数据库中包含哪些模块. 如果要创建第二个数据库并将某些模块表移动到第二个数据库,则需要有第二个`MigrationsDbContext` 类,该类仅调用相关模块的扩展方法. 下一部分将详细介绍该主题.
+
+同样 `ConfigureBackgroundJobs` 方法也被后台作业模块的 `DbContext` 调用:
+
+````csharp
+[ConnectionStringName(BackgroundJobsDbProperties.ConnectionStringName)]
+public class BackgroundJobsDbContext
+ : AbpDbContext, IBackgroundJobsDbContext
+{
+ public DbSet BackgroundJobs { get; set; }
+
+ public BackgroundJobsDbContext(DbContextOptions options)
+ : base(options)
+ {
+
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+
+ //Reuse the same extension method!
+ builder.ConfigureBackgroundJobs();
+ }
+}
+````
+
+通过这种方式,可以在 `DbContext` 类之间共享模块的映射配置. 上面的代码在相关的模块NuGet包中,所以你不需要关心它.
+
+##### 重用模块的表
+
+您可能想在应用程序中**重用依赖模块的表**. 在这种情况下你有两个选择:
+
+1. 你可以**直接使用模块定义的实体**.
+2. 你可以**创建一个新的实体**映射到同一个数据库表。
+
+###### 使用由模块定义的实体
+
+使用实体定义的模块有标准用法非常简单. 例如身份模块定义了 `IdentityUser` 实体. 你可以为注入 `IdentityUser` 仓储,为此实体执行标准仓储操作.
+例:
+
+````csharp
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Domain.Repositories;
+using Volo.Abp.Identity;
+
+namespace Acme.BookStore
+{
+ public class MyService : ITransientDependency
+ {
+ private readonly IRepository _identityUserRepository;
+
+ public MyService(IRepository identityUserRepository)
+ {
+ _identityUserRepository = identityUserRepository;
+ }
+
+ public async Task DoItAsync()
+ {
+ //Get all users
+ var users = await _identityUserRepository.GetListAsync();
+ }
+ }
+}
+````
+
+示例注入了 `IRepository`(默认仓储). 它定义了标准的存储库方法并实现了 `IQueryable` 接口.
+
+另外,身份模块定义了 `IIdentityUserRepository`(自定义仓储),你的应用程序也可以注入和使用它. `IIdentityUserRepository` 为 `IdentityUser` 实体提供了额外的定制方法,但它没有实现 `IQueryable`.
+
+###### 创建一个新的实体
+
+如果你只是按原样使用实体,那么使用模块的实体是很容易的. 但是在下面的情况下你可能需要定义自己的实体并映射到模块实体相同的数据库表.
+
+* 你想要**添加一个新的字段**到表中并将其映射到实体的属性. 你无法使用模块的实体,因为它没有相关的属性.
+* 你想要**使用的表字段的一个子集**. 你不希望访问实体的所有属性并隐藏不相关的属性(从安全的角度或仅出于设计目的).
+* 你不希望直接**依赖于**模块实体类.
+
+无论哪一种情况,流程都是一样的. 假如你想要创建一个名为 `AppRole` 的实体,将其映射到与[身份模块](Modules/Identity.md)的 `IdentityRole` 实体相同的表
+
+在这里我们将介绍实现方式, 然后**将讨论这种方法的局限性**.
+
+首先,在你的 `.Domain` 项目一个新的 `AppRole` 类:
+
+````csharp
+using System;
+using Volo.Abp.Domain.Entities;
+using Volo.Abp.MultiTenancy;
+
+namespace Acme.BookStore.Roles
+{
+ public class AppRole : AggregateRoot, IMultiTenant
+ {
+ // Properties shared with the IdentityRole class
+
+ public Guid? TenantId { get; private set; }
+ public string Name { get; private set; }
+
+ //Additional properties
+
+ public string Title { get; set; }
+
+ private AppRole()
+ {
+
+ }
+ }
+}
+````
+
+* 它继承了[`AggregateRoot`类](Entities.md)和实现了[`IMultiTenant`]接口(Multi-Tenancy.md),因为 `IdentityRole` 也做了同样的继承.
+* 你可以添加 `IdentityRole` 实体定义的任何属性. 本例只加了 `TenantId` 和 `Name` 属性,因为我们这里只需要它们. 你可以把setters设置为私有(如同本例)以防意外更改身份模块的属性.
+* 你可以添加自定义(附加)属性. 本例添加了 `Title` 属性.
+* **构造函数是私有的**,所以它不允许直接创建一个新的 `AppRole` 实体。创建角色身份模块的责任. 你可以查询角色,设置/更新自定义属性,但做为最佳实践你不应该在代码中创建和删除角色(尽管没有强制的限制).
+
+现在是时候定义EF Core映射. 打开应用程序的 `DbContext` (此示例中是 `BookStoreDbContext` )添加以下属性:
+
+````csharp
+public DbSet Roles { get; set; }
+````
+
+然后在 `OnModelCreating` 方法中配置映射(调用 `base.OnModelCreating(builder)` 之后):
+
+````csharp
+protected override void OnModelCreating(ModelBuilder builder)
+{
+ base.OnModelCreating(builder);
+
+ /* Configure the shared tables (with included modules) here */
+
+ //CONFIGURE THE AppRole ENTITY
+ builder.Entity(b =>
+ {
+ b.ToTable("AbpRoles");
+
+ b.ConfigureByConvention();
+
+ b.ConfigureCustomRoleProperties();
+ });
+
+ ...
+
+ /* Configure your own tables/entities inside the ConfigureBookStore method */
+
+ builder.ConfigureBookStore();
+}
+````
+
+我们添加了以下几行:
+
+````csharp
+builder.Entity(b =>
+{
+ b.ToTable("AbpRoles");
+
+ b.ConfigureByConvention();
+
+ b.ConfigureCustomRoleProperties();
+});
+````
+
+* 它映射到 `AbpRoles` 表,与 `IdentityRole` 实体共享.
+* `ConfigureByConvention()` 配置了标准/基本属性(像`TenantId`),建议总是调用它.
+
+`ConfigureCustomRoleProperties()` 还不存在. 在 `BookStoreDbContextModelCreatingExtensions` 类中定义它 (在 `.EntityFrameworkCore` 项目的 `DbContext` 附近):
+
+````csharp
+public static void ConfigureCustomRoleProperties(this EntityTypeBuilder b)
+ where TRole : class, IEntity
+{
+ b.Property(nameof(AppRole.Title)).HasMaxLength(128);
+}
+````
+
+* 这个方法只定义实体的**自定义属性**.
+* 遗憾的是,我们不能在这里充分的利用**类型安全**(通过引用`AppRole`实体). 我们能做的最好就是使用 `Title` 名称做为类型安全。
+
+你已经为运行应用程序使用的 `DbContext` 配置了自定义属性. 我们还需要配置 `MigrationsDbContext`.
+
+打开`MigrationsDbContext`(本例是 `BookStoreMigrationsDbContext`)进行以下更改:
+
+````csharp
+protected override void OnModelCreating(ModelBuilder builder)
+{
+ base.OnModelCreating(builder);
+
+ /* Include modules to your migration db context */
+
+ ...
+
+ /* Configure customizations for entities from the modules included */
+
+ //CONFIGURE THE CUSTOM ROLE PROPERTIES
+ builder.Entity(b =>
+ {
+ b.ConfigureCustomRoleProperties();
+ });
+
+ ...
+
+ /* Configure your own tables/entities inside the ConfigureBookStore method */
+
+ builder.ConfigureBookStore();
+}
+````
+
+只增加下面几行:
+
+````csharp
+builder.Entity(b =>
+{
+ b.ConfigureCustomRoleProperties();
+});
+````
+
+通过这种方式,我们重用了用于为角色配置自定义属性映射的扩展方法. 但是对 `IdentityRole` 实体进行了相同的自定义.
+
+现在你可以在包管理控制台(记得选择 `.EntityFrameworkCore.DbMigrations` 做为PMC的默认项目并将 `.Web` 项目设置为启动项目)使用标准的 `Add-Migration` 命令添加一个新的EF Core数据库迁移.
+
+
+
+命令将创建一个新的Code First迁移类,如下所示:
+
+````csharp
+public partial class Added_Title_To_Roles : Migration
+{
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "Title",
+ table: "AbpRoles",
+ maxLength: 128,
+ nullable: true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Title",
+ table: "AbpRoles");
+ }
+}
+````
+
+全部完成,只运行在PMC运行`Update-Database` 命令或运行你解决方案中的`.DbMigrator` 项目应用数据库更改.
+
+现在你可以像使用应用程序的其他实体一样使用 `AppRole` 实体. 下面是查询和更新角色的[应用服务](Application-Services.md).
+示例:
+
+````csharp
+public class AppRoleAppService : ApplicationService, IAppRoleAppService
+{
+ private readonly IRepository _appRoleRepository;
+
+ public AppRoleAppService(IRepository appRoleRepository)
+ {
+ _appRoleRepository = appRoleRepository;
+ }
+
+ public async Task> GetListAsync()
+ {
+ var roles = await _appRoleRepository.GetListAsync();
+
+ return roles
+ .Select(r => new AppRoleDto
+ {
+ Id = r.Id,
+ Name = r.Name,
+ Title = r.Title
+ })
+ .ToList();
+ }
+
+ public async Task UpdateTitleAsync(Guid id, string title)
+ {
+ var role = await _appRoleRepository.GetAsync(id);
+
+ role.Title = title;
+
+ await _appRoleRepository.UpdateAsync(role);
+ }
+}
+````
+
+创建新实体并将其映射到依赖模块的表存在一些**限制**:
+
+* 你的**自定义属性必须是可空的**. 例如 `AppRole.Title` 在这里是可空的. 否则身份模块会抛出异常,因为它在向数据库插入新角色时不知道如何如何填充标题.
+* 好的做法是,你不应更新**模块定义的属性**,尤其是在需要业务逻辑的情况下. 通常你可以管理自己(自定义)的属性.
+
+##### 替代方法
+
+你可以使用以下方法替代创建实体添加自定义属性.
+
+###### 使用ExtraProperties
+
+所有从 `AggregateRoot` 派生的实体都可以在 `ExtraProperties` 属性中存储键值对, 它是 `Dictionary` 类型在数据库中被序列化为JSON. 所以你可以在字典中添加值用于查询,无需更改实体.
+
+例如你可以将查询属性 `Title` 存储在 `IdentityRole` 中,而不是创建一个新的实体.
+例:
+
+````csharp
+public class IdentityRoleExtendingService : ITransientDependency
+{
+ private readonly IIdentityRoleRepository _identityRoleRepository;
+
+ public IdentityRoleExtendingService(IIdentityRoleRepository identityRoleRepository)
+ {
+ _identityRoleRepository = identityRoleRepository;
+ }
+
+ public async Task GetTitleAsync(Guid id)
+ {
+ var role = await _identityRoleRepository.GetAsync(id);
+
+ return role.GetProperty("Title");
+ }
+
+ public async Task SetTitleAsync(Guid id, string newTitle)
+ {
+ var role = await _identityRoleRepository.GetAsync(id);
+
+ role.SetProperty("Title", newTitle);
+
+ await _identityRoleRepository.UpdateAsync(role);
+ }
+}
+````
+
+* `GetProperty` 和 `SetProperty` 方法是在 `role.ExtraProperties` 词典中获取和设置值的简化写法,推荐使用.
+
+通过这种方式你可以轻松任何类型的值附加到依赖模块的实体. 不过这种方法也有一些缺点:
+
+* 所有的额外属性都存储在数据库中的一个**JSON对象**,它们不是作为表的字段存储,与简单的表字段相比创建索引和针对此属性使用SQL查询将更加困难.
+* 属性名称是字符串,他们**不是类型安全的**. 建议这些类型的属性定义常量,以防止拼写错误.
+
+###### 创建新表
+
+你可以创建**自己的表**来存储属性,而不是创建新实体并映射到同一表. 你通常复制原始实体的一些值. 例如可以将 `Name` 字段添加到你自己的表中,它是原表中 `Name` 字段的副本.
+
+在这种情况下你不需要处理迁移问题,但是需要处理数据复制问题. 当重复的值发生变化时,你应该在表中同步相同的变化. 你可以使用本地或分布式[事件总线](Event-Bus.md)订阅原始实体的更改事件. 这是根据来自另一个微服务的数据推荐的方法,特别是如果它们有单独的物理数据库(你可以在网络中搜索关于微服务设计的数据共享,这是一个广泛的主题).
+
+#### 讨论另一种场景:每个模块管理自己的迁移路径
+
+上面提到, `.EntityFrameworkCore.DbMigrations` 合并所有模块的所有数据库映射(加上应用程序映射)创建统一的迁移路径.
+
+另一种方法是允许每个模块拥有自己的迁移来维护其数据库表. 虽然它在一开始看起来更模块化,但它有一些重要的缺点:
+
+* **EF Core迁移系统依赖于数据库DBMS提供程序**. 例如,如果一个模块为SQL Server创建迁移,那么你就不能使用MySQL使用这个迁移代码,对于模块而言为所有可用的DBMS提供程序维护迁移是不切实际的. 将迁移留给应用程序代码(如本文档中所解释的),你就可以在**应用程序代码中选择DBMS**.
+* 在模块之间 **共享表** 或在应用程序中 **重用模块的表** 会更加困难. 因为EF Core迁移系统不能处理它,并会抛出异常,例如"数据库中已存在表XXX".
+* **自定义/增强** 映射和生成迁移代码会更加困难.
+* 当你使用多个模块时更难跟踪和**应用数据库更改**.
+
+## 使用多个数据库
+
+默认的启动模板被组织为所有模块和应用程序使用单个数据库. 但是ABP框架和所有预构建的模块都被设计为**可以使用多个数据库**. 每个模块可以使用自己的数据库,也可以将模块分组到几个数据库中.
+
+本节将说明如何将审计日志,设置管理和权限管理模块表移至**第二个数据库**,而其余模块继续使用主("Default")数据库.
+
+结构如下图所示:
+
+
+
+### 更改连接字符串部分
+
+第一步是更改所有 `appsettings.json` 文件的连接字符串部分. 最初它是这样的:
+
+````json
+"ConnectionStrings": {
+ "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true"
+}
+````
+
+更改如下所示:
+
+````json
+"ConnectionStrings": {
+ "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true",
+ "AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true",
+ "AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true",
+ "AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true"
+}
+````
+
+为相关模块添加了**另外三个连接字符串**,以针对 `BookStore_SecondDb` 数据库(它们都是相同的). 例如 `AbpPermissionManagement` 是权限管理模块的连接字符串.
+
+`AbpPermissionManagement` 是权限管理模块[定义](https://github.com/abpframework/abp/blob/97eaa6ff5a044f503465455c86332e5a277b077a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDbProperties.cs#L11)的一个常量. 如果定义值,ABP框架[连接字符串选择系统](Connection-Strings.md)将为权限管理模块选择此连接字符串. 如果不定义它将回退到 `Default` 连接字符串.
+
+### 创建第二个迁移项目
+
+在**运行时**上定义如上所述的连接字符串就足够了. 但是 `BookStore_SecondDb` 数据库还不存在. 你需要为相关模块创建数据库表.
+
+就像主数据库我们要使用的EF Core Code Frist迁移系统创建和维护第二个数据库.
+
+一种简单的方法是为第二个迁移 `DbContext` 创建第二个项目(`.csproj`).
+
+在你的解决方案中创建一个名为 `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` 的**类库项目**(如果不喜欢你可以进行其他命名).
+
+`.csproj` 内容应该和以下类似:
+
+````xml
+
+
+
+
+
+ netcoreapp3.1
+ Acme.BookStore.DbMigrationsForSecondDb
+
+
+
+
+
+
+
+
+
+
+
+````
+
+你可以仅复制和修改原始 `.DbMigrations` 项目的内容. 该项目引用 `.EntityFrameworkCore` 项目. **唯一的区别**是 `RootNamespace` 值.
+
+从 `.Web` 项目中添加对该项目的引用(否则,EF Core Tool不允许使用`Add-migration`命令).
+
+### 创建第二个DbMigrationDbContext
+
+为迁移创建一个新的 `DbContext` 并调用模块的**扩展方法**为相关模块配置数据库表:
+
+````csharp
+[ConnectionStringName("AbpPermissionManagement")]
+public class BookStoreSecondMigrationsDbContext :
+ AbpDbContext
+{
+ public BookStoreSecondMigrationsDbContext(
+ DbContextOptions options)
+ : base(options)
+ {
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+
+ /* Include modules to your migration db context */
+
+ builder.ConfigurePermissionManagement();
+ builder.ConfigureSettingManagement();
+ builder.ConfigureAuditLogging();
+ }
+}
+````
+
+> `[ConnectionStringName(...)` Attribute 在这里非常重,它告诉ABP框架应该为这个 `DbContext` 使用哪个连接字符串. 我们使用了 `AbpPermissionManagement`.
+
+创建一个**Design Time Db Factory**类, EF Core Tool需要使用它(例如`Add-Migration`和`Update-Database` PCM命令):
+
+````csharp
+/* This class is needed for EF Core console commands
+ * (like Add-Migration and Update-Database commands) */
+public class BookStoreSecondMigrationsDbContextFactory
+ : IDesignTimeDbContextFactory
+{
+ public BookStoreSecondMigrationsDbContext CreateDbContext(string[] args)
+ {
+ var configuration = BuildConfiguration();
+
+ var builder = new DbContextOptionsBuilder()
+ .UseSqlServer(configuration.GetConnectionString("AbpPermissionManagement"));
+
+ return new BookStoreSecondMigrationsDbContext(builder.Options);
+ }
+
+ private static IConfigurationRoot BuildConfiguration()
+ {
+ var builder = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile("appsettings.json", optional: false);
+
+ return builder.Build();
+ }
+}
+````
+
+它与 `.EntityFrameworCore.DbMigrations` 项目中的类相似, 除了它使用 `AbpPermissionManagement` 连接字符串.
+
+现在你可以打开包管理器控制台, 选择 `.EntityFrameworkCore.DbMigrationsForSecondDb` 项目为默认项目(确保`.Web`项目是启动项目),运行 `Add-Migration "Initial` 和 `Update-Database` 命令,如下所示:
+
+
+
+现在你应该有一个包含相关数据库表的新数据库.
+
+
+
+### 从主数据库中删除模块
+
+我们 **创建第二个数据库** 包含了审计日志,权限管理和设置管理模块表. 我们应该从**主数据库中删除这些表**,这是很容易的.
+S
+首先移除从 `MigrationsDbContext` 类以下行(本例中是`BookStoreMigrationsDbContext`):
+
+````csharp
+builder.ConfigurePermissionManagement();
+builder.ConfigureSettingManagement();
+builder.ConfigureAuditLogging();
+````
+
+打开包管理器控制台, 选择 `.EntityFrameworkCore.DbMigrations` 为默认的项目(确保`.Web`项目是启动项目)运行以下命令:
+
+````
+Add-Migration "Removed_Audit_Setting_Permission_Modules"
+````
+
+此命令将创建一个如下所示的迁移类:
+
+````csharp
+public partial class Removed_Audit_Setting_Permission_Modules : Migration
+{
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "AbpAuditLogActions");
+
+ migrationBuilder.DropTable(
+ name: "AbpEntityPropertyChanges");
+
+ migrationBuilder.DropTable(
+ name: "AbpPermissionGrants");
+
+ migrationBuilder.DropTable(
+ name: "AbpSettings");
+
+ migrationBuilder.DropTable(
+ name: "AbpEntityChanges");
+
+ migrationBuilder.DropTable(
+ name: "AbpAuditLogs");
+ }
+
+ ...
+}
+````
+
+在这一步需要小心:
+
+* 如果你是 **已经在生产的系统**, 那么你应该关心**数据丢失**,在删除表之前需要将表的内容移动到第二个数据库.
+* 如果你还 **没有开始** 你的项目, 你可以考虑 **删除所有的迁移** 并且重新生成初始版本,让迁移历史更加清晰.
+
+运行 `Update-Database` 命令从主数据库中删除表.
+
+请注意如果你还没有将其复制到新数据库,那么还会**删除了一些初始种子数据**(例如授予管理员角色的权限). 如果你运行应用程序可能无法再登录. 解决方案很简单:在解决方案中重新运行 `.DbMigrator` 控制台应用程序,它会为新数据库添加种子数据.
+
+### 自动化第二个数据库模式迁移
+
+`.DbMigrator` 控制台应用程序可以跨多个数据库运行数据库种子代码无需任何其他配置. 但它不能在第二个数据库迁移项目中运行EF Core First迁移. 下面介绍如何配置控制台迁移应用程序以处理两个数据库.
+
+#### 实现 IBookStoreDbSchemaMigrator
+
+`Acme.BookStore.EntityFrameworkCore.DbMigrations` 项目中的 `EntityFrameworkCoreBookStoreDbSchemaMigrator` 类负责为 `BookStoreMigrationsDbContext` 迁移数据库模式. 它应该是这样的:
+
+````csharp
+[Dependency(ReplaceServices = true)]
+public class EntityFrameworkCoreBookStoreDbSchemaMigrator
+ : IBookStoreDbSchemaMigrator, ITransientDependency
+{
+ private readonly IServiceProvider _serviceProvider;
+
+ public EntityFrameworkCoreBookStoreDbSchemaMigrator(
+ IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public async Task MigrateAsync()
+ {
+ /* We are intentionally resolving the BookStoreMigrationsDbContext
+ * from IServiceProvider (instead of directly injecting it)
+ * to properly get the connection string of the current tenant in the
+ * current scope.
+ */
+
+ await _serviceProvider
+ .GetRequiredService()
+ .Database
+ .MigrateAsync();
+ }
+}
+````
+
+它实现了 `IBookStoreDbSchemaMigrator` 并**替换现有服务**(见第一行).
+
+删除 `[Dependency(ReplaceServices = true)]`,因为我们有这个接口的两个实现并且希望同时使用它们.
+
+在新的迁移项目(`Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb`)创建它的一个副本,内部使用 `BookStoreSecondMigrationsDbContext` . 示例实现:
+
+````csharp
+public class EntityFrameworkCoreSecondBookStoreDbSchemaMigrator
+ : IBookStoreDbSchemaMigrator, ITransientDependency
+{
+ private readonly IServiceProvider _serviceProvider;
+
+ public EntityFrameworkCoreSecondBookStoreDbSchemaMigrator(
+ IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public async Task MigrateAsync()
+ {
+ /* We are intentionally resolving the BookStoreSecondMigrationsDbContext
+ * from IServiceProvider (instead of directly injecting it)
+ * to properly get the connection string of the current tenant in the
+ * current scope.
+ */
+
+ await _serviceProvider
+ .GetRequiredService()
+ .Database
+ .MigrateAsync();
+ }
+}
+````
+
+> 类的名称对于[依赖注入](Dependency-Injection.md)非常重要. 为了可以使用 `IBookStoreDbSchemaMigrator` 引用注入它的结尾应该是 `BookStoreDbSchemaMigrator`.
+
+现在我们有 `IBookStoreDbSchemaMigrator` 接口的两个实现,每个实现负责迁移相关的数据库模式.
+
+#### 为第二个迁移项目定义一个模块类
+
+是时候为第二次迁移(`Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb`)项目定义[模块](Module-Development-Basics.md)类了:
+
+````csharp
+[DependsOn(
+ typeof(BookStoreEntityFrameworkCoreModule)
+ )]
+public class BookStoreEntityFrameworkCoreSecondDbMigrationsModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddAbpDbContext();
+ }
+}
+````
+
+在 `Acme.BookStore.DbMigrator` 项目中引用 `Acme.BookStore.EntityFrameworkCore.DbMigrationsForSecondDb` 项目并且将 `typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule)` 依赖项添加到 `BookStoreDbMigratorModule` 的依赖项列表中. `BookStoreDbMigratorModule` 类应该是这样的:
+
+````csharp
+[DependsOn(
+ typeof(AbpAutofacModule),
+ typeof(BookStoreEntityFrameworkCoreDbMigrationsModule),
+ typeof(BookStoreEntityFrameworkCoreSecondDbMigrationsModule), // ADDED THIS!
+ typeof(BookStoreApplicationContractsModule)
+ )]
+public class BookStoreDbMigratorModule : AbpModule
+{
+ ...
+}
+````
+
+#### BookStoreDbMigrationService
+
+最后还需要找到 `Acme.BookStore.Domain` 项目的 `BookStoreDbMigrationService` 类,它当前被设计为使用单个 `IBookStoreDbSchemaMigrator` 实现,但现在我们有了两个.
+
+它注入了 `IBookStoreDbSchemaMigrator`. 使用 `IEnumerable` 注入替换它, ([依赖注入系统](Dependency-Injection.md) 允许像这样注入接口的多个实现).
+
+现在你有 **模式迁移器的集合** ,找到以下行:
+
+````csharp
+await _dbSchemaMigrators.MigrateAsync();
+````
+
+将这行代码进行以下改变:
+
+````csharp
+foreach (var migrator in _dbSchemaMigrators)
+{
+ await migrator.MigrateAsync();
+}
+````
+
+你可以运行 `.DbMigrator` 应用程序应用迁移和初始化种子数据. 你可以删除这两个数据库进行测试,然后再次运行 `.DbMigrator` 应用程序,它会创建两个数据库.
+
+## 结论
+
+本文档说明了如何拆分数据库以及管理Entity Framework Core解决方案的数据库迁移. 简而言之,你需要为每个不同的数据库创建一个单独的迁移项目.
\ No newline at end of file
diff --git a/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md b/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md
index 943667eb55..a1af1c5849 100644
--- a/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md
+++ b/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md
@@ -2,9 +2,13 @@
**[应用程序启动模板](Startup-Templates/Application.md)** 为EF Core预配置了Sql Server提供程序,EF Core支持许多其它DBMS,你可以在基于ABP的应用程序使用它们.
-ABP框架为一些常见的DMBS提供了简化配置的集成包(有关可用集成包的列表,请参阅[EF Core文档](Entity-Framework-Core.md)),你也可以不使用集成包配置DBMS提供程序.
+ABP框架为一些常见的DMBS提供了简化配置的**集成包**,你可以通过以下文档来学习如何**切换到你喜欢的DBMS**:
-虽然总是建议使用集成包(它也使不同模块之间的依赖版本成为标准版本),但是如果没有用于DBMS提供程序的集成包,也可以手动集成.
+* [MySQL](Entity-Framework-Core-MySQL.md)
+* [PostgreSQL](Entity-Framework-Core-PostgreSQL.md)
+* [SQLite](Entity-Framework-Core-SQLite.md)
+
+你也可以不使用集成包配置DBMS提供程序,虽然总是建议使用集成包(它也使不同模块之间的依赖版本成为标准版本),但是如果没有用于DBMS提供程序的集成包,也可以手动集成.
本文介绍了如何在不使用[MySQL集成包](Entity-Framework-Core-MySQL.md)的情况下切换到MySQL.
diff --git a/docs/zh-Hans/Entity-Framework-Core.md b/docs/zh-Hans/Entity-Framework-Core.md
index b70630e38f..0bec4d1ab9 100644
--- a/docs/zh-Hans/Entity-Framework-Core.md
+++ b/docs/zh-Hans/Entity-Framework-Core.md
@@ -32,14 +32,9 @@ namespace MyCompany.MyProject
EF Core支持多种数据库管理系统([查看全部](https://docs.microsoft.com/en-us/ef/core/providers/)). ABP框架和本文档不依赖于任何特定的DBMS.
-如果要创建一个可重用的库,应避免依赖于特定的DBMS包.但在最终的应用程序中,始终会选择一个DBMS.
+如果要创建一个可重用的[应用程序模块](Modules/Index.md),应避免依赖于特定的DBMS包.但在最终的应用程序中,始终会选择一个DBMS.
-ABP框架为一些常见的DBMS提供了集成包,使配置变得更加简单. [启动模板](Startup-Templates/Index.md)附带**预先配置的SQL Server (localdb)**.请参阅以下文档,了解如何配置其他DBMS提供程序:
-
-* [MySQL](Entity-Framework-Core-MySQL.md)
-* [PostgreSQL](Entity-Framework-Core-PostgreSQL.md)
-* [SQLite](Entity-Framework-Core-SQLite.md)
-* [Others](Entity-Framework-Core-Other-DBMS.md)
+参阅[为Entity Framework Core切换到其他DBMS](Entity-Framework-Core-Other-DBMS.md)文档学习如何切换DBMS.
## 创建 DbContext
diff --git a/docs/zh-Hans/Exception-Handling.md b/docs/zh-Hans/Exception-Handling.md
index a251c50f61..c1132d4e3c 100644
--- a/docs/zh-Hans/Exception-Handling.md
+++ b/docs/zh-Hans/Exception-Handling.md
@@ -190,7 +190,7 @@ throw new UserFriendlyException(_stringLocalizer["UserNameShouldBeUniqueMessage"
"UserNameShouldBeUniqueMessage": "Username should be unique! '{0}' is already taken!"
````
-* `IUserFriendlyException`接口派生自`IBusinessException`,而 `UserFriendlyException `类派生自`BusinessException`类.
+* `IUserFriendlyException`接口派生自`IBusinessException`,而 `UserFriendlyException`类派生自`BusinessException`类.
#### 使用错误代码
diff --git a/docs/zh-Hans/Modules/Identity.md b/docs/zh-Hans/Modules/Identity.md
new file mode 100644
index 0000000000..ea50006de9
--- /dev/null
+++ b/docs/zh-Hans/Modules/Identity.md
@@ -0,0 +1,3 @@
+# 身份管理模块
+
+参阅 [源码](https://github.com/abpframework/abp/tree/dev/modules/identity). 文档很快会被完善.
\ No newline at end of file
diff --git a/docs/zh-Hans/Modules/Tentant-Management.md b/docs/zh-Hans/Modules/Tentant-Management.md
new file mode 100644
index 0000000000..fa999428dd
--- /dev/null
+++ b/docs/zh-Hans/Modules/Tentant-Management.md
@@ -0,0 +1,3 @@
+# 租户管理模块
+
+TODO
\ No newline at end of file
diff --git a/docs/zh-Hans/Multi-Tenancy.md b/docs/zh-Hans/Multi-Tenancy.md
index 83d190bb38..da5f1d74b6 100644
--- a/docs/zh-Hans/Multi-Tenancy.md
+++ b/docs/zh-Hans/Multi-Tenancy.md
@@ -303,6 +303,7 @@ TODO:...
Volo.Abp.AspNetCore.MultiTenancy 添加了下面这些租户解析器,从当前Web请求(按优先级排序)中确定当前租户.
+* **CurrentUserTenantResolveContributor**: 如果当前用户已登录,从当前用户的声明中获取租户Id. **出于安全考虑,应该始终将其做为第一个Contributor**.
* **QueryStringTenantResolver**: 尝试从query string参数中获取当前租户,默认参数名为"__tenant".
* **RouteTenantResolver**:尝试从当前路由中获取(URL路径),默认是变量名是"__tenant".所以,如果你的路由中定义了这个变量,就可以从路由中确定当前租户.
* **HeaderTenantResolver**: 尝试从HTTP header中获取当前租户,默认的header名称是"__tenant".
diff --git a/docs/zh-Hans/Nightly-Builds.md b/docs/zh-Hans/Nightly-Builds.md
index c37c9af9bc..3d34330ca3 100644
--- a/docs/zh-Hans/Nightly-Builds.md
+++ b/docs/zh-Hans/Nightly-Builds.md
@@ -1,8 +1,8 @@
- # 每日构建
+# 每日构建
-所有框架和模块包都每晚都部署到MyGet. 因此你可以使用或测试最新的代码,而无需等待下一个版本.
+所有框架和模块包每晚都部署到MyGet. 因此你可以使用或测试最新的代码,而无需等待下一个版本.
-## 在Visual Studio配置
+## 在Visual Studio配置
> 需要Visual Studio 2017以上
diff --git a/docs/zh-Hans/Tutorials/Angular/Part-I.md b/docs/zh-Hans/Tutorials/Angular/Part-I.md
new file mode 100644
index 0000000000..8e8c74ec59
--- /dev/null
+++ b/docs/zh-Hans/Tutorials/Angular/Part-I.md
@@ -0,0 +1,3 @@
+## Angular 教程 - 第一章
+
+TODO...
\ No newline at end of file
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md
index 5394890f8d..afb1afc069 100644
--- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md
+++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-I.md
@@ -16,7 +16,7 @@
### 创建项目
-创建一个名为`Acme.BookStore`的新项目, 创建数据库并按照[入门文档](../../Getting-Started-AspNetCore-MVC-Template.md)运行应用程序.
+创建一个名为`Acme.BookStore`的新项目, 创建数据库并按照[入门文档](../../../Getting-Started-AspNetCore-MVC-Template.md)运行应用程序.
### 解决方案的结构
@@ -24,16 +24,16 @@

-> 你可以查看[应用程序模板文档](../../Startup-Templates/Application.md)以详细了解解决方案结构.但是,你将通过本教程了解基础知识.
+> 你可以查看[应用程序模板文档](../../../Startup-Templates/Application.md)以详细了解解决方案结构.但是,你将通过本教程了解基础知识.
### 创建Book实体
启动模板中的域层分为两个项目:
- - `Acme.BookStore.Domain`包含你的[实体](../../Entities.md), [领域服务](../../Domain-Services.md)和其他核心域对象.
+ - `Acme.BookStore.Domain`包含你的[实体](../../../Entities.md), [领域服务](../../../Domain-Services.md)和其他核心域对象.
- `Acme.BookStore.Domain.Shared`包含可与客户共享的常量,枚举或其他域相关对象.
-在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](../../Entities.md). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示:
+在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](../../../Entities.md). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示:
````C#
using System;
@@ -66,7 +66,7 @@ namespace Acme.BookStore
}
````
-* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是**域驱动设计(DDD)** 概念之一. 有关详细信息和最佳做法,请参阅[实体文档](../../Entities.md).
+* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是**域驱动设计(DDD)** 概念之一. 有关详细信息和最佳做法,请参阅[实体文档](../../../Entities.md).
* `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些审计属性(`CreationTime`, `CreatorId`, `LastModificationTime` 等).
* `Guid`是`Book`实体的主键类型.
* 使用 **数据注解** 为EF Core添加映射.或者你也可以使用 EF Core 自带的[fluent mapping API](https://docs.microsoft.com/en-us/ef/core/modeling).
@@ -138,7 +138,7 @@ PM> Update-Database
### 创建应用服务
-下一步是创建[应用服务](../../Application-Services.md)来管理(创建,列出,更新,删除)书籍. 启动模板中的应用程序层分为两个项目:
+下一步是创建[应用服务](../../../Application-Services.md)来管理(创建,列出,更新,删除)书籍. 启动模板中的应用程序层分为两个项目:
* `Acme.BookStore.Application.Contracts`主要包含你的DTO和应用程序服务接口.
* `Acme.BookStore.Application`包含应用程序服务的实现.
@@ -166,7 +166,7 @@ namespace Acme.BookStore
}
````
-* **DTO**类被用来在 **表示层** 和 **应用层** **传递数据**.查看[DTO文档](../../Data-Transfer-Objects.md)查看更多信息.
+* **DTO**类被用来在 **表示层** 和 **应用层** **传递数据**.查看[DTO文档](../../../Data-Transfer-Objects.md)查看更多信息.
* 为了在页面上展示书籍信息,`BookDto`被用来将书籍数据传递到表示层.
* `BookDto`继承自 `AuditedEntityDto`.跟上面定义的`Book`类一样具有一些审计属性.
@@ -217,7 +217,7 @@ namespace Acme.BookStore
````
* 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息.
-* 它定义了数据注释属性(如`[Required]`)来定义属性的验证. DTO由ABP框架[自动验证](../../Validation.md).
+* 它定义了数据注释属性(如`[Required]`)来定义属性的验证. DTO由ABP框架[自动验证](../../../Validation.md).
就像上面的`BookDto`一样,创建一个从`CreateUpdateBookDto`对象到`Book`实体的映射:
@@ -281,12 +281,12 @@ namespace Acme.BookStore
````
* `BookAppService`继承了`CrudAppService<...>`.它实现了上面定义的CRUD方法.
-* `BookAppService`注入`IRepository `,这是`Book`实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅[仓储文档](../../Repositories.md)
+* `BookAppService`注入`IRepository `,这是`Book`实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅[仓储文档](../../../Repositories.md)
* `BookAppService`使用`IObjectMapper`将`Book`对象转换为`BookDto`对象, 将`CreateUpdateBookDto`对象转换为`Book`对象. 启动模板使用[AutoMapper](http://automapper.org/)库作为对象映射提供程序. 你之前定义了映射, 因此它将按预期工作.
### 自动生成API Controllers
-你通常创建**Controller**以将应用程序服务公开为**HTTP API**端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. ABP可以[**自动**](../../AspNetCore/Auto-API-Controllers.md)按照惯例将你的应用程序服务配置为MVC API控制器.
+你通常创建**Controller**以将应用程序服务公开为**HTTP API**端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. ABP可以[**自动**](../../../AspNetCore/Auto-API-Controllers.md)按照惯例将你的应用程序服务配置为MVC API控制器.
#### Swagger UI
@@ -392,7 +392,7 @@ context.Menu.AddItem(
}
````
-* ABP的本地化功能建立在[ASP.NET Core's standard localization]((https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization))之上并增加了一些扩展.查看[本地化文档](../../Localization.md).
+* ABP的本地化功能建立在[ASP.NET Core's standard localization]((https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization))之上并增加了一些扩展.查看[本地化文档](../../../Localization.md).
* 本地化key是任意的. 你可以设置任何名称. 我们更喜欢为菜单项添加`Menu:`前缀以区别于其他文本. 如果未在本地化文件中定义文本,则它将**返回**到本地化的key(ASP.NET Core的标准行为).
运行该应用程序,看到新菜单项已添加到顶部栏:
@@ -437,8 +437,8 @@ context.Menu.AddItem(
````
-* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro)用于将外部的 **脚本** 添加到页面中.它比标准的`script`标签多了很多额外的功能.它可以处理 **最小化**和 **版本**.查看[捆绑 & 压缩文档](../../AspNetCore/Bundling-Minification.md)获取更多信息.
-* `abp-card` 和 `abp-table` 是为Twitter Bootstrap的[card component](http://getbootstrap.com/docs/4.1/components/card/)封装的 **tag helpers**.ABP中有很多tag helpers,可以很方便的使用大多数[bootstrap](https://getbootstrap.com/)组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看[tag helpers 文档](../../AspNetCore/Tag-Helpers/Index.md).
+* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro)用于将外部的 **脚本** 添加到页面中.它比标准的`script`标签多了很多额外的功能.它可以处理 **最小化**和 **版本**.查看[捆绑 & 压缩文档](../../../AspNetCore/Bundling-Minification.md)获取更多信息.
+* `abp-card` 和 `abp-table` 是为Twitter Bootstrap的[card component](http://getbootstrap.com/docs/4.1/components/card/)封装的 **tag helpers**.ABP中有很多tag helpers,可以很方便的使用大多数[bootstrap](https://getbootstrap.com/)组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看[tag helpers 文档](../../../AspNetCore/Tag-Helpers/Index.md).
* 你可以像上面本地化菜单一样 **本地化** 列名.
#### 添加脚本文件
diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md
index e48cf6b710..a5e51134fa 100644
--- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md
+++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-III.md
@@ -68,7 +68,7 @@ namespace Acme.BookStore
````
* 注入`IRepository`并在`SeedAsync`中使用它来创建两个书实体作为测试数据.
-* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../../Guid-Generation.md)了解更多信息).
+* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../../../Guid-Generation.md)了解更多信息).
### 测试 BookAppService
diff --git a/docs/zh-Hans/Tutorials/Index.md b/docs/zh-Hans/Tutorials/Index.md
new file mode 100644
index 0000000000..f7eea987e4
--- /dev/null
+++ b/docs/zh-Hans/Tutorials/Index.md
@@ -0,0 +1,6 @@
+# 教程
+
+## 应用开发
+
+* [使用ASP.NET Core MVC/ Razor Pages UI](AspNetCore-Mvc/Part-I.md)
+* [使用Angular UI](Angular/Part-I.md)
diff --git a/docs/zh-Hans/_resources/Diagrams.docx b/docs/zh-Hans/_resources/Diagrams.docx
new file mode 100644
index 0000000000..424e281470
Binary files /dev/null and b/docs/zh-Hans/_resources/Diagrams.docx differ
diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json
index ad811cc5b3..f67da497c3 100644
--- a/docs/zh-Hans/docs-nav.json
+++ b/docs/zh-Hans/docs-nav.json
@@ -29,6 +29,7 @@
},
{
"text": "教程",
+ "path": "Tutorials/Index.md",
"items": [
{
"text": "应用开发",
@@ -277,20 +278,27 @@
"path": "Entity-Framework-Core.md",
"items": [
{
- "text": "切换到MySql",
- "path": "Entity-Framework-Core-MySQL.md"
+ "text": "数据库迁移",
+ "path": "Entity-Framework-Core-Migrations.md"
},
{
- "text": "切换到PostgreSQL",
- "path": "Entity-Framework-Core-PostgreSQL.md"
- },
- {
- "text": "切换到SQLite",
- "path": "Entity-Framework-Core-SQLite.md"
- },
- {
- "text": "切换到其他DBMS",
- "path": "Entity-Framework-Core-Other-DBMS.md"
+
+ "text": "切换DMBS",
+ "path": "Entity-Framework-Core-Other-DBMS.md",
+ "items":[
+ {
+ "text": "到MySql",
+ "path": "Entity-Framework-Core-MySQL.md"
+ },
+ {
+ "text": "到PostgreSQL",
+ "path": "Entity-Framework-Core-PostgreSQL.md"
+ },
+ {
+ "text": "到SQLite",
+ "path": "Entity-Framework-Core-SQLite.md"
+ }
+ ]
}
]
},
@@ -320,11 +328,39 @@
{
"text": "RabbitMQ 集成",
"path": "Background-Jobs-RabbitMq.md"
+ },
+ {
+ "text": "Quartz 集成",
+ "path": "Background-Jobs-Quartz.md"
+ }
+ ]
+ },
+ {
+ "text": "后台工作者",
+ "path": "Background-Workers.md",
+ "items": [
+ {
+ "text": "Quartz 集成",
+ "path": "Background-Workers-Quartz.md"
}
]
}
]
},
+ {
+ "text": "启动模板",
+ "path": "Startup-Templates/Index.md",
+ "items": [
+ {
+ "text": "应用程序",
+ "path": "Startup-Templates/Application.md"
+ },
+ {
+ "text": "模块",
+ "path": "Startup-Templates/Module.md"
+ }
+ ]
+ },
{
"text": "示例",
"items": [
diff --git a/docs/zh-Hans/images/bookstore-efcore-dependencies.png b/docs/zh-Hans/images/bookstore-efcore-dependencies.png
new file mode 100644
index 0000000000..3de5a679ef
Binary files /dev/null and b/docs/zh-Hans/images/bookstore-efcore-dependencies.png differ
diff --git a/docs/zh-Hans/images/bookstore-second-database.png b/docs/zh-Hans/images/bookstore-second-database.png
new file mode 100644
index 0000000000..3faf19496c
Binary files /dev/null and b/docs/zh-Hans/images/bookstore-second-database.png differ
diff --git a/docs/zh-Hans/images/multiple-database-usage.png b/docs/zh-Hans/images/multiple-database-usage.png
new file mode 100644
index 0000000000..ada6177629
Binary files /dev/null and b/docs/zh-Hans/images/multiple-database-usage.png differ
diff --git a/docs/zh-Hans/images/pmc-add-migration-initial-update-database.png b/docs/zh-Hans/images/pmc-add-migration-initial-update-database.png
new file mode 100644
index 0000000000..3cc45e5ddf
Binary files /dev/null and b/docs/zh-Hans/images/pmc-add-migration-initial-update-database.png differ
diff --git a/docs/zh-Hans/images/pmc-add-migration-role-title.png b/docs/zh-Hans/images/pmc-add-migration-role-title.png
new file mode 100644
index 0000000000..69fc5b7bc5
Binary files /dev/null and b/docs/zh-Hans/images/pmc-add-migration-role-title.png differ
diff --git a/docs/zh-Hans/images/single-database-usage.png b/docs/zh-Hans/images/single-database-usage.png
new file mode 100644
index 0000000000..108173fd86
Binary files /dev/null and b/docs/zh-Hans/images/single-database-usage.png differ
diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln
index b731aff187..88c8385b63 100644
--- a/framework/Volo.Abp.sln
+++ b/framework/Volo.Abp.sln
@@ -1,4 +1,5 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28922.388
MinimumVisualStudioVersion = 10.0.40219.1
@@ -252,13 +253,27 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.ObjectMapping.Test
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Ddd.Application.Contracts", "src\Volo.Abp.Ddd.Application.Contracts\Volo.Abp.Ddd.Application.Contracts.csproj", "{73559227-EBF0-475F-835B-1FF0CD9132AA}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Minify", "src\Volo.Abp.Minify\Volo.Abp.Minify.csproj", "{928DC30D-C078-4BB4-A9F8-FE7252C67DC6}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Minify", "src\Volo.Abp.Minify\Volo.Abp.Minify.csproj", "{928DC30D-C078-4BB4-A9F8-FE7252C67DC6}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Minify.Tests", "test\Volo.Abp.Minify.Tests\Volo.Abp.Minify.Tests.csproj", "{E69182B3-350A-43F5-A935-5EBBEBECEF97}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Minify.Tests", "test\Volo.Abp.Minify.Tests\Volo.Abp.Minify.Tests.csproj", "{E69182B3-350A-43F5-A935-5EBBEBECEF97}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Serilog", "src\Volo.Abp.AspNetCore.Serilog\Volo.Abp.AspNetCore.Serilog.csproj", "{3B801003-BE74-49ED-9749-DA5E99F45EBF}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Serilog", "src\Volo.Abp.AspNetCore.Serilog\Volo.Abp.AspNetCore.Serilog.csproj", "{3B801003-BE74-49ED-9749-DA5E99F45EBF}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Serilog.Tests", "test\Volo.Abp.AspNetCore.Serilog.Tests\Volo.Abp.AspNetCore.Serilog.Tests.csproj", "{9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Serilog.Tests", "test\Volo.Abp.AspNetCore.Serilog.Tests\Volo.Abp.AspNetCore.Serilog.Tests.csproj", "{9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Http.Client.IdentityModel.Web", "src\Volo.Abp.Http.Client.IdentityModel.Web\Volo.Abp.Http.Client.IdentityModel.Web.csproj", "{925AF101-2203-409C-9C3B-03917316858F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.BackgroundJobs.Quartz", "src\Volo.Abp.BackgroundJobs.Quartz\Volo.Abp.BackgroundJobs.Quartz.csproj", "{2307198B-5837-4F05-AA84-D6EC2A923D69}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Quartz", "src\Volo.Abp.Quartz\Volo.Abp.Quartz.csproj", "{9467418B-4A9B-4093-9B31-01A9DEF5B372}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.BackgroundWorkers.Quartz", "src\Volo.Abp.BackgroundWorkers.Quartz\Volo.Abp.BackgroundWorkers.Quartz.csproj", "{CD5770BB-2E0C-4B3C-80E0-21B8CC43DBA9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo", "src\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj", "{29E42ADB-85F8-44AE-A9B0-078F84C1B866}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo", "test\Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo\Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.csproj", "{0C498CF2-D052-4BF7-AD35-509A90F69707}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Http.Client.IdentityModel.Web.Tests", "test\Volo.Abp.Http.Client.IdentityModel.Web.Tests\Volo.Abp.Http.Client.IdentityModel.Web.Tests.csproj", "{E1963439-2BE5-4DB5-8438-2A9A792A1ADA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -774,6 +789,34 @@ Global
{9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}.Release|Any CPU.Build.0 = Release|Any CPU
+ {925AF101-2203-409C-9C3B-03917316858F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {925AF101-2203-409C-9C3B-03917316858F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {925AF101-2203-409C-9C3B-03917316858F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {925AF101-2203-409C-9C3B-03917316858F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2307198B-5837-4F05-AA84-D6EC2A923D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2307198B-5837-4F05-AA84-D6EC2A923D69}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2307198B-5837-4F05-AA84-D6EC2A923D69}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2307198B-5837-4F05-AA84-D6EC2A923D69}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9467418B-4A9B-4093-9B31-01A9DEF5B372}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9467418B-4A9B-4093-9B31-01A9DEF5B372}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9467418B-4A9B-4093-9B31-01A9DEF5B372}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9467418B-4A9B-4093-9B31-01A9DEF5B372}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CD5770BB-2E0C-4B3C-80E0-21B8CC43DBA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CD5770BB-2E0C-4B3C-80E0-21B8CC43DBA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CD5770BB-2E0C-4B3C-80E0-21B8CC43DBA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CD5770BB-2E0C-4B3C-80E0-21B8CC43DBA9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {29E42ADB-85F8-44AE-A9B0-078F84C1B866}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {29E42ADB-85F8-44AE-A9B0-078F84C1B866}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {29E42ADB-85F8-44AE-A9B0-078F84C1B866}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {29E42ADB-85F8-44AE-A9B0-078F84C1B866}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0C498CF2-D052-4BF7-AD35-509A90F69707}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0C498CF2-D052-4BF7-AD35-509A90F69707}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0C498CF2-D052-4BF7-AD35-509A90F69707}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0C498CF2-D052-4BF7-AD35-509A90F69707}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E1963439-2BE5-4DB5-8438-2A9A792A1ADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E1963439-2BE5-4DB5-8438-2A9A792A1ADA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E1963439-2BE5-4DB5-8438-2A9A792A1ADA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E1963439-2BE5-4DB5-8438-2A9A792A1ADA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -906,6 +949,13 @@ Global
{E69182B3-350A-43F5-A935-5EBBEBECEF97} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{3B801003-BE74-49ED-9749-DA5E99F45EBF} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62} = {447C8A77-E5F0-4538-8687-7383196D04EA}
+ {925AF101-2203-409C-9C3B-03917316858F} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
+ {2307198B-5837-4F05-AA84-D6EC2A923D69} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
+ {9467418B-4A9B-4093-9B31-01A9DEF5B372} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
+ {CD5770BB-2E0C-4B3C-80E0-21B8CC43DBA9} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
+ {29E42ADB-85F8-44AE-A9B0-078F84C1B866} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
+ {0C498CF2-D052-4BF7-AD35-509A90F69707} = {447C8A77-E5F0-4538-8687-7383196D04EA}
+ {E1963439-2BE5-4DB5-8438-2A9A792A1ADA} = {447C8A77-E5F0-4538-8687-7383196D04EA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs
index 44e9076751..cc62a1e1b9 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs
@@ -1,4 +1,5 @@
using System;
+using Volo.Abp.AspNetCore.Mvc.MultiTenancy;
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
{
@@ -14,5 +15,9 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations
public CurrentUserDto CurrentUser { get; set; }
public ApplicationFeatureConfigurationDto Features { get; set; }
+
+ public MultiTenancyInfoDto MultiTenancy { get; set; }
+
+ public CurrentTenantDto CurrentTenant { get; set; }
}
}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/MultiTenancy/CurrentTenantDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/MultiTenancy/CurrentTenantDto.cs
new file mode 100644
index 0000000000..06b24f1235
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/MultiTenancy/CurrentTenantDto.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Volo.Abp.AspNetCore.Mvc.MultiTenancy
+{
+ public class CurrentTenantDto
+ {
+ public Guid? Id { get; set; }
+
+ public string Name { get; set; }
+
+ public bool IsAvailable { get; set; }
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/MultiTenancy/MultiTenancyInfoDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/MultiTenancy/MultiTenancyInfoDto.cs
new file mode 100644
index 0000000000..165a0e5c21
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/MultiTenancy/MultiTenancyInfoDto.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Volo.Abp.AspNetCore.Mvc.MultiTenancy
+{
+ public class MultiTenancyInfoDto
+ {
+ public bool IsEnabled { get; set; }
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs
index ee2a920980..6f8ccf285a 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpDynamicformTagHelperService.cs
@@ -47,7 +47,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
SetFormAttributes(context, output);
- SetSubmitButton(context, output);
+ await SetSubmitButton(context, output);
}
protected virtual async Task ConvertToMvcForm(TagHelperContext context, TagHelperOutput output)
@@ -107,14 +107,14 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form
output.Content.SetHtmlContent(childContent);
}
- protected virtual void SetSubmitButton(TagHelperContext context, TagHelperOutput output)
+ protected virtual async Task SetSubmitButton(TagHelperContext context, TagHelperOutput output)
{
if (!TagHelper.SubmitButton ?? true)
{
return;
}
- var buttonHtml = ProcessSubmitButtonAndGetContentAsync(context, output);
+ var buttonHtml = await ProcessSubmitButtonAndGetContentAsync(context, output);
output.PostContent.SetHtmlContent(output.PostContent.GetContent() + buttonHtml);
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/AbpAspNetCoreMvcUiThemeSharedDemoModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/AbpAspNetCoreMvcUiThemeSharedDemoModule.cs
new file mode 100644
index 0000000000..cd312db532
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/AbpAspNetCoreMvcUiThemeSharedDemoModule.cs
@@ -0,0 +1,19 @@
+using Volo.Abp.Modularity;
+using Volo.Abp.VirtualFileSystem;
+
+namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo
+{
+ [DependsOn(
+ typeof(AbpAspNetCoreMvcUiThemeSharedModule)
+ )]
+ public class AbpAspNetCoreMvcUiThemeSharedDemoModule : AbpModule
+ {
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded("Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo");
+ });
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/FodyWeavers.xml
new file mode 100644
index 0000000000..00e1d9a1c1
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/FodyWeavers.xsd b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/FodyWeavers.xsd
similarity index 100%
rename from modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.HangFire/FodyWeavers.xsd
rename to framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/FodyWeavers.xsd
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/AlertsDemo/AlertsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/AlertsDemo/AlertsDemoViewComponent.cs
new file mode 100644
index 0000000000..2599c317cf
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/AlertsDemo/AlertsDemoViewComponent.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp.AspNetCore.Mvc.UI.Widgets;
+
+namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.AlertsDemo
+{
+ [Widget]
+ public class AlertsDemoViewComponent : AbpViewComponent
+ {
+ public const string ViewPath = "/Views/Components/Themes/Shared/Demos/AlertsDemo/Default.cshtml";
+
+ public IViewComponentResult Invoke()
+ {
+ return View(ViewPath);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/AlertsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/AlertsDemo/Default.cshtml
new file mode 100644
index 0000000000..8c132b9197
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/AlertsDemo/Default.cshtml
@@ -0,0 +1,70 @@
+@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.AlertsDemo
+
+
+
+ A simple primary alert—check it out!
+
+
+ A simple secondary alert—check it out!
+
+
+ A simple success alert—check it out!
+
+
+ A simple danger alert—check it out!
+
+
+ A simple warning alert—check it out!
+
+
+ A simple info alert—check it out!
+
+
+ A simple light alert—check it out!
+
+
+ A simple dark alert—check it out!
+
+
+
+
+
+ A simple primary alert with an example link. Give it a click if you like.
+
+
+ A simple secondary alert with an example link. Give it a click if you like.
+
+
+ A simple success alert with an example link. Give it a click if you like.
+
+
+ A simple danger alert with an example link. Give it a click if you like.
+
+
+ A simple warning alert with an example link. Give it a click if you like.
+
+
+ A simple info alert with an example link. Give it a click if you like.
+
+
+ A simple light alert with an example link. Give it a click if you like.
+
+
+ A simple dark alert with an example link. Give it a click if you like.
+
+
+
+
+
+
Well done!
+
Aww yeah, you successfully read this important alert message. This example text is going to run a bit longer so that you can see how spacing within an alert works with this kind of content.
+
+
Whenever you need to, be sure to use margin utilities to keep things nice and tidy.
+
+
+
+
+
+ Holy guacamole! You should check in on some of those fields below.
+
+
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BadgesDemo/BadgesDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BadgesDemo/BadgesDemoViewComponent.cs
new file mode 100644
index 0000000000..bdc47336cd
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BadgesDemo/BadgesDemoViewComponent.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp.AspNetCore.Mvc.UI.Widgets;
+
+namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BadgesDemo
+{
+ [Widget]
+ public class BadgesDemoViewComponent : AbpViewComponent
+ {
+ public const string ViewPath = "/Views/Components/Themes/Shared/Demos/BadgesDemo/Default.cshtml";
+
+ public IViewComponentResult Invoke()
+ {
+ return View(ViewPath);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BadgesDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BadgesDemo/Default.cshtml
new file mode 100644
index 0000000000..9ed21ebe3e
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BadgesDemo/Default.cshtml
@@ -0,0 +1,49 @@
+@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BadgesDemo
+
+
+
Example heading New
+
Example heading New
+
Example heading New
+
Example heading New
+
Example heading New
+
Example heading New
+
+
+
+
+ Notifications 4
+
+
+
+
+ Primary
+ Secondary
+ Success
+ Danger
+ Warning
+ Info
+ Light
+ Dark
+
+
+
+ Primary
+ Secondary
+ Success
+ Danger
+ Warning
+ Info
+ Light
+ Dark
+
+
+
+ Primary
+ Secondary
+ Success
+ Danger
+ Warning
+ Info
+ Light
+ Dark
+
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BordersDemo/BordersDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BordersDemo/BordersDemoViewComponent.cs
new file mode 100644
index 0000000000..2ba33feb54
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BordersDemo/BordersDemoViewComponent.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp.AspNetCore.Mvc.UI.Widgets;
+
+namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BordersDemo
+{
+ [Widget]
+ public class BordersDemoViewComponent : AbpViewComponent
+ {
+ public const string ViewPath = "/Views/Components/Themes/Shared/Demos/BordersDemo/Default.cshtml";
+
+ public IViewComponentResult Invoke()
+ {
+ return View(ViewPath);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BordersDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BordersDemo/Default.cshtml
new file mode 100644
index 0000000000..281f6c73ed
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BordersDemo/Default.cshtml
@@ -0,0 +1,46 @@
+@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BordersDemo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BreadcrumbsDemo/BreadcrumbsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BreadcrumbsDemo/BreadcrumbsDemoViewComponent.cs
new file mode 100644
index 0000000000..e700acb667
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BreadcrumbsDemo/BreadcrumbsDemoViewComponent.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp.AspNetCore.Mvc.UI.Widgets;
+
+namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BreadcrumbsDemo
+{
+ [Widget]
+ public class BreadcrumbsDemoViewComponent : AbpViewComponent
+ {
+ public const string ViewPath = "/Views/Components/Themes/Shared/Demos/BreadcrumbsDemo/Default.cshtml";
+
+ public IViewComponentResult Invoke()
+ {
+ return View(ViewPath);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BreadcrumbsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BreadcrumbsDemo/Default.cshtml
new file mode 100644
index 0000000000..8338bdfc1f
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/BreadcrumbsDemo/Default.cshtml
@@ -0,0 +1,18 @@
+@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.BreadcrumbsDemo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonGroupsDemo/ButtonGroupsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonGroupsDemo/ButtonGroupsDemoViewComponent.cs
new file mode 100644
index 0000000000..ff5b917d40
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonGroupsDemo/ButtonGroupsDemoViewComponent.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp.AspNetCore.Mvc.UI.Widgets;
+
+namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ButtonGroupsDemo
+{
+ [Widget]
+ public class ButtonGroupsDemoViewComponent : AbpViewComponent
+ {
+ public const string ViewPath = "/Views/Components/Themes/Shared/Demos/ButtonGroupsDemo/Default.cshtml";
+
+ public IViewComponentResult Invoke()
+ {
+ return View(ViewPath);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonGroupsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonGroupsDemo/Default.cshtml
new file mode 100644
index 0000000000..30c6eb5ef1
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonGroupsDemo/Default.cshtml
@@ -0,0 +1,75 @@
+@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ButtonGroupsDemo
+
+
+
+ Left
+ Middle
+ Right
+
+
+
+
+
+
+ 1
+ 2
+ 3
+ 4
+
+
+ 5
+ 6
+ 7
+
+
+ 8
+
+
+
+
+
+
+ Left
+ Middle
+ Right
+
+
+ Left
+ Middle
+ Right
+
+
+ Left
+ Middle
+ Right
+
+
+
+
+
+ 1
+ 2
+
+
+
+ Dropdown link
+ Dropdown link
+
+
+
+
+
+
+
+ Button
+ Button
+ Button
+
+
+
+ Dropdown link
+ Dropdown link
+
+
+
+
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonsDemo/ButtonsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonsDemo/ButtonsDemoViewComponent.cs
new file mode 100644
index 0000000000..9d7a2dd3f9
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonsDemo/ButtonsDemoViewComponent.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp.AspNetCore.Mvc.UI.Widgets;
+
+namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ButtonsDemo
+{
+ [Widget]
+ public class ButtonsDemoViewComponent : AbpViewComponent
+ {
+ public const string ViewPath = "/Views/Components/Themes/Shared/Demos/ButtonsDemo/Default.cshtml";
+
+ public IViewComponentResult Invoke()
+ {
+ return View(ViewPath);
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonsDemo/Default.cshtml
new file mode 100644
index 0000000000..39b079f802
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/ButtonsDemo/Default.cshtml
@@ -0,0 +1,55 @@
+@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ButtonsDemo
+
+
+
+
+ Secondary
+ Success
+ Danger
+ Warning
+ Info
+ Light
+ Dark
+ Link
+
+
+
+ Primary
+ Secondary
+ Success
+ Danger
+ Warning
+ Info
+ Light
+ Dark
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CardsDemo/CardsDemoViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CardsDemo/CardsDemoViewComponent.cs
new file mode 100644
index 0000000000..f192285f80
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CardsDemo/CardsDemoViewComponent.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp.AspNetCore.Mvc.UI.Widgets;
+
+namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.CardsDemo
+{
+ [Widget]
+ public class CardsDemoViewComponent : AbpViewComponent
+ {
+ public const string ViewPath = "/Views/Components/Themes/Shared/Demos/CardsDemo/Default.cshtml";
+
+ public IViewComponentResult Invoke()
+ {
+ return View(ViewPath);
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CardsDemo/Default.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CardsDemo/Default.cshtml
new file mode 100644
index 0000000000..d40637735c
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/Demos/CardsDemo/Default.cshtml
@@ -0,0 +1,305 @@
+@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.CardsDemo
+
+
+
+
+
+ Card Title
+ Some quick example text to build on the card title and make up the bulk of the card's content.
+ Go somewhere
+
+
+
+
+
+
+
+ Card title
+ Card subtitle
+ Some quick example text to build on the card title and make up the bulk of the card's content.
+ Card link
+ Another link
+
+
+
+
+
+
+
+ Cras justo odio
+ Dapibus ac facilisis in
+ Vestibulum at eros
+
+
+
+
+
+
+ Featured
+
+ Cras justo odio
+ Dapibus ac facilisis in
+ Vestibulum at eros
+
+
+
+
+
+
+
+
+ Card TitleSome quick example text to build on the card title and make up the bulk of the card's content.
+
+
+ Cras justo odio
+ Dapibus ac facilisis in
+ Vestibulum at eros
+
+
+ Card link
+ Another link
+
+
+
+
+
+
+ Featured
+
+ Special title treatment
+ With supporting text below as a natural lead-in to additional content.
+ Go somewhere
+
+
+
+
+
+
+ Quote
+
+
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.
+
+
+
+
+
+
+
+
+ Featured
+
+ Special title treatment
+ With supporting text below as a natural lead-in to additional content.
+ Go somewhere
+
+ 2 days ago
+
+
+
+
+