diff --git a/.gitignore b/.gitignore
index eb4dbedfae..9a855fd898 100644
--- a/.gitignore
+++ b/.gitignore
@@ -293,3 +293,10 @@ 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
samples/MicroserviceDemo/microservices/TenantManagementService.Host/Logs/logs.txt
+/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package-lock.json
+/samples/BookStore/src/Acme.BookStore.Web/package-lock.json
+/samples/DashboardDemo/src/DashboardDemo.Web/package-lock.json
+/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/package-lock.json
+/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/package-lock.json
+/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/package-lock.json
+/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/package-lock.json
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
index a5089e06f7..14a4eb345c 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
@@ -3,6 +3,12 @@
"texts": {
"Permission:Organizations": "Organizations",
"Permission:Manage": "Manage Organizations",
+ "Permission:DiscountRequests": "Discount Requests",
+ "Permission:DiscountManage": "Manage Discount Requests",
+ "Permission:Disable": "Disable",
+ "Permission:Enable": "Enable",
+ "Permission:EnableSendEmail": "Enable Send Email",
+ "Permission:SendEmail": "Send Email",
"Permission:NpmPackages": "NPM Packages",
"Permission:NugetPackages": "Nuget Packages",
"Permission:Maintenance": "Maintenance",
@@ -13,9 +19,13 @@
"Permission:Edit": "Edit",
"Permission:Delete": "Delete",
"Permission:Create": "Create",
+ "Permission:Accounting": "Accounting",
+ "Permission:Accounting:Quotation": "Quotation",
+ "Permission:Accounting:Invoice": "Invoice",
"Menu:Organizations": "Organizations",
- "Menu:Accounting": "Accounting",
+ "Menu:Accounting": "Accounting",
"Menu:Packages": "Packages",
+ "Menu:DiscountRequests": "Discount Requests",
"NpmPackageDeletionWarningMessage": "This NPM Package will be deleted. Do you confirm that?",
"NugetPackageDeletionWarningMessage": "This Nuget Package will be deleted. Do you confirm that?",
"ModuleDeletionWarningMessage": "This Module will be deleted. Do you confirm that?",
@@ -94,13 +104,49 @@
"UsernameOrEmail": "Username or email",
"UsernameOrEmailPlaceholder": "Username or email...",
"Member": "Member",
- "QuotationPurchasedOrderNo": "Quotation Purchased Order No",
- "QuotationTime": "Quotation Time",
- "CompanyName": "Company Name",
- "CompanyAddress": "Company Address",
+ "PurchaseOrderNo": "Purchase order no",
+ "QuotationDate": "Quotation date",
+ "CompanyName": "Company name",
+ "CompanyAddress": "Company address",
"Price": "Price",
+ "DiscountText": "Discount text",
+ "DiscountQuantity": "Discount quantity",
+ "DiscountPrice": "Discount price",
+ "Quotation": "Quotation",
"ExtraText": "Extra Text",
"ExtraAmount": "Extra Amount",
- "DownloadQuotation": "Download Quotation"
+ "DownloadQuotation": "Download Quotation",
+ "Invoice": "Invoice",
+ "TaxNumber": "Tax Number",
+ "InvoiceNumber": "Invoice Number",
+ "InvoiceDate": "Invoice Date",
+ "Quantity": "Quantity",
+ "AddProduct": "Add Product",
+ "AddProductWarning": "You need to add product!",
+ "TotalPrice": "Total Price",
+ "Generate": "Generate",
+ "MissingQuantityField": "The quantity field is required!",
+ "MissingPriceField": "The Price field is required!",
+ "CodeUsageStatus": "Code Usage Status",
+ "Country": "Country",
+ "DeveloperCount": "Developer Count",
+ "RequestCode": "Request Code",
+ "WebSite": "Web Site",
+ "GithubUsername": "Github Username",
+ "PhoneNumber": "Phone Number",
+ "ProjectDescription": "Project Description",
+ "Referrer": "Referrer",
+ "DiscountRequests": "Discount Request",
+ "Copylink": "Copy Link",
+ "Disable": "Disable",
+ "Enable": "Enable",
+ "EnableSendEmail": "Enable Send Email",
+ "SendEmail": "Send Email",
+ "SuccessfullyDisabled": "Successfully Disabled",
+ "SuccessfullyEnabled": "Successfully Enabled",
+ "EmailSent": "Email Sent",
+ "SuccessfullySent": "Successfully Sent",
+ "SuccessfullyDeleted": "Successfully Deleted",
+ "DiscountRequestDeletionWarningMessage": "Discount request will be deleted"
}
}
\ No newline at end of file
diff --git a/common.DotSettings b/common.DotSettings
index 0eb4875d49..6f40d029a7 100644
--- a/common.DotSettings
+++ b/common.DotSettings
@@ -20,5 +20,11 @@
FalseFalseSQL
+ False
+ False
+ False
+ False
+ False
+ FalseTrue
\ No newline at end of file
diff --git a/common.props b/common.props
index 19b6f81fe6..ab7b86e3b2 100644
--- a/common.props
+++ b/common.props
@@ -1,7 +1,7 @@
latest
- 2.7.0
+ 2.8.0$(NoWarn);CS1591https://abp.io/assets/abp_nupkg.pnghttps://abp.io
diff --git a/docs/cs/Entity-Framework-Core-PostgreSQL.md b/docs/cs/Entity-Framework-Core-PostgreSQL.md
index 3a39c7485d..e09880c37c 100644
--- a/docs/cs/Entity-Framework-Core-PostgreSQL.md
+++ b/docs/cs/Entity-Framework-Core-PostgreSQL.md
@@ -10,9 +10,9 @@ Projekt `.EntityFrameworkCore` v řešení závisí na NuGet balíku [Volo.Abp.E
Najděte třídu ***YourProjectName*EntityFrameworkCoreModule** v projektu `.EntityFrameworkCore`, odstraňte `typeof(AbpEntityFrameworkCoreSqlServerModule)` z atributu `DependsOn`, přidejte `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (také nahraďte `using Volo.Abp.EntityFrameworkCore.SqlServer;` za `using Volo.Abp.EntityFrameworkCore.PostgreSql;`).
-## UsePostgreSql()
+## UseNpgsql()
-Najděte volání `UseSqlServer()` v *YourProjectName*EntityFrameworkCoreModule.cs uvnitř projektu `.EntityFrameworkCore` a nahraďte za `UsePostgreSql()`.
+Najděte volání `UseSqlServer()` v *YourProjectName*EntityFrameworkCoreModule.cs uvnitř projektu `.EntityFrameworkCore` a nahraďte za `UseNpgsql()`.
Najděte volání `UseSqlServer()` v *YourProjectName*MigrationsDbContextFactory.cs uvnitř projektu `.EntityFrameworkCore.DbMigrations` a nahraďte za `UseNpgsql()`.
diff --git a/docs/cs/Getting-Started-AspNetCore-Application.md b/docs/cs/Getting-Started-AspNetCore-Application.md
index 2e8ea1a343..877976d037 100644
--- a/docs/cs/Getting-Started-AspNetCore-Application.md
+++ b/docs/cs/Getting-Started-AspNetCore-Application.md
@@ -57,7 +57,7 @@ namespace BasicAspNetCoreApplication
app.UseStaticFiles();
app.UseRouting();
- app.UseMvcWithDefaultRouteAndArea();
+ app.UseConfiguredEndpoints();
}
}
}
diff --git a/docs/en/API/Dynamic-CSharp-API-Clients.md b/docs/en/API/Dynamic-CSharp-API-Clients.md
index 460623440f..94a0222243 100644
--- a/docs/en/API/Dynamic-CSharp-API-Clients.md
+++ b/docs/en/API/Dynamic-CSharp-API-Clients.md
@@ -39,7 +39,7 @@ Now, it's ready to create the client proxies. Example:
````csharp
[DependsOn(
typeof(AbpHttpClientModule), //used to create client proxies
- typeof(BookStoreApplicationModule) //contains the application service interfaces
+ typeof(BookStoreApplicationContractsModule) //contains the application service interfaces
)]
public class MyClientAppModule : AbpModule
{
@@ -47,7 +47,7 @@ public class MyClientAppModule : AbpModule
{
//Create dynamic client proxies
context.Services.AddHttpClientProxies(
- typeof(BookStoreApplicationModule).Assembly
+ typeof(BookStoreApplicationContractsModule).Assembly
);
}
}
@@ -142,12 +142,12 @@ The examples above have configured the "Default" remote service endpoint. You ma
````csharp
context.Services.AddHttpClientProxies(
- typeof(BookStoreApplicationModule).Assembly,
- remoteServiceName: "BookStore"
+ typeof(BookStoreApplicationContractsModule).Assembly,
+ remoteServiceConfigurationName: "BookStore"
);
````
-`remoteServiceName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint.
+`remoteServiceConfigurationName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint.
### As Default Services
@@ -155,7 +155,7 @@ When you create a service proxy for `IBookAppService`, you can directly inject t
````csharp
context.Services.AddHttpClientProxies(
- typeof(BookStoreApplicationModule).Assembly,
+ typeof(BookStoreApplicationContractsModule).Assembly,
asDefaultServices: false
);
````
diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/Post.md b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/Post.md
new file mode 100644
index 0000000000..b6c1d3f952
--- /dev/null
+++ b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/Post.md
@@ -0,0 +1,247 @@
+# ABP Framework v2.7.0 Has Been Released!
+
+The **ABP Framework** & and the **ABP Commercial** v2.7 have been released. We hadn't created blog post for the 2.4, 2.4 and 2.6 releases, so this post will also cover **what's new** with these releases and **what we've done** in the last 2 months.
+
+## About the Release Cycle & Development
+
+Reminding that we had started to release a new minor feature version **in every two weeks**, generally on Thursdays. Our goal is to deliver new features as soon as possible.
+
+We've completed & merged hundreds of issues and pull requests with **1,300+ commits** in the last 7-8 weeks, only for the ABP Framework repository. Daily commit counts are constantly increasing:
+
+
+
+ABP.IO Platform is rapidly growing and we are getting more and more contributions from the community.
+
+## What's New in the ABP Framework?
+
+### Object Extending System
+
+In the last few releases, we've mostly focused on providing ways to extend existing modules when you use them as NuGet/NPM Packages.
+
+The Object Extending System allows module developers to create extensible modules and allows application developers to customize and extend a module easily.
+
+For example, you can add two extension properties to the user entity of the identity module:
+
+````csharp
+ObjectExtensionManager.Instance
+ .AddOrUpdate(options =>
+ {
+ options.AddOrUpdateProperty("SocialSecurityNumber");
+ options.AddOrUpdateProperty("IsSuperUser");
+ }
+ );
+````
+
+It is easy to define validation rules for the properties:
+
+````csharp
+ObjectExtensionManager.Instance
+ .AddOrUpdateProperty(
+ "SocialSecurityNumber",
+ options =>
+ {
+ options.Attributes.Add(new RequiredAttribute());
+ options.Attributes.Add(
+ new StringLengthAttribute(32) {
+ MinimumLength = 6
+ }
+ );
+ });
+````
+
+You can even write custom code to validate the property. It automatically works for the objects those are parameters of an application service, controller or a page.
+
+While extension properties of an entity are normally stored in a single JSON formatted field in the database table, you can easily configure to store a property as a table field using the EF Core mapping:
+
+````csharp
+ObjectExtensionManager.Instance
+ .AddOrUpdateProperty(
+ "SocialSecurityNumber",
+ options =>
+ {
+ options.MapEfCore(b => b.HasMaxLength(32));
+ }
+ );
+````
+
+See the [Object Extensions document](https://docs.abp.io/en/abp/latest/Object-Extensions) for details about this system.
+
+See also the [Customizing the Existing Modules](https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Guide) guide to learn all the possible customization options.
+
+### Text Templating Package
+
+[Volo.Abp.TextTemplating](https://www.nuget.org/packages/Volo.Abp.TextTemplating) is a new package introduced with the v2.7.0. Previously, [Volo.Abp.Emailing](https://www.nuget.org/packages/Volo.Abp.Emailing) package had a similar functionality but it was limited, experimental and tightly coupled to the emailing.
+
+The new text templating package allows you to define text based templates those can be easily localized and reused. You can define layout templates and share the layout from other templates.
+
+We are currently using it for email sending. A module needs to send an email typically defines a template. Example:
+
+````xml
+
+````
+
+This is a typical password reset email template.
+
+* The template system is based on the open source [Scriban library](https://github.com/lunet-io/scriban). So it supports if conditions, loops and much more.
+* `model` is used to pass data to the template (just like the ASP.NET Core MVC).
+* `L` is a special function that localizes the given string.
+
+It is typical to use the same layout for all emails. So, you can define a layout template. This is the standard layout template comes with the framework:
+
+````xml
+
+
+
+
+
+
+ {{content}}
+
+
+````
+
+A layout should have a `{{content}}` area to render the child content (just like the `RenderBody()` in the MVC).
+
+It is very easy to override a template content by the final application to customize it.
+
+Whenever you need to render a template, use the `ITemplateRenderer` service by providing the template name and a model. See the [text templating documentation](https://docs.abp.io/en/abp/latest/Text-Templating) for details. We've even created a UI for the ABP Commercial (see the related section below).
+
+### Subscribing to the Exceptions
+
+ABP Framework's [exception handling system](https://docs.abp.io/en/abp/latest/Exception-Handling) automatically handles exceptions and returns an appropriate result to the client. In some cases, you may want to have a callback that is notified whenever an exception occurs. In this way, for example, you can send an email or take any action based on the exception.
+
+Just create a class derived from the `ExceptionSubscriber` class in your application:
+
+````csharp
+public class MyExceptionSubscriber : ExceptionSubscriber
+{
+ public override async Task HandleAsync(ExceptionNotificationContext context)
+ {
+ //TODO...
+ }
+}
+````
+
+See the [exception handling](https://docs.abp.io/en/abp/latest/Exception-Handling) document for more.
+
+### Others
+
+There are many minor features and enhancements made to the framework in the past releases. Here, a few ones:
+
+* Added `AbpLocalizationOptions.DefaultResourceType` to set the default resource type for the application. In this way, the localization system uses the default resource whenever the resource was not specified. The latest application startup template already configures it, but you may want to set it for your existing applications.
+* Added `IsEnabled` to permission definition. In this way, you can completely disable a permission and hide the related functionality from the application. This can be a way of feature switch for some applications. See [#3486](https://github.com/abpframework/abp/issues/3486) for usage.
+* Added Dutch and German localizations to all the localization resources defined by the framework. Thanks to the contributors.
+
+## What's New in the ABP Commercial
+
+The goal of the [ABP Commercial](https://commercial.abp.io/) is to provide pre-build application functionalities, code generation tools, professional themes, advanced samples and premium support for ABP Framework based projects.
+
+We are working on the ABP Commercial in the parallel to align with the ABP Framework features and provide more modules, theme options and tooling.
+
+This section explains what's going on the ABP Commercial side.
+
+### Module Entity Extension System
+
+Module entity extension system is a higher level API that uses the object extension system (introduced above) and provides an easy way to add extension properties to existing entities. A new extension property easily automatically becomes a part of the HTTP API and the User Interface.
+
+Example: Add a `SocialSecurityNumber` to the user entity of the identity module
+
+````csharp
+ObjectExtensionManager.Instance.Modules()
+ .ConfigureIdentity(identity =>
+ {
+ identity.ConfigureUser(user =>
+ {
+ user.AddOrUpdateProperty( //property type: string
+ "SocialSecurityNumber", //property name
+ property =>
+ {
+ //validation rules
+ property.Attributes.Add(new RequiredAttribute());
+ property.Attributes.Add(
+ new StringLengthAttribute(64) {
+ MinimumLength = 4
+ }
+ );
+
+ //...other configurations for this property
+ }
+ );
+ });
+ });
+````
+
+With just such a configuration, the user interface will have the new property (on the table and on the create/edit forms):
+
+
+
+The new property can be easily localized and validated. Currently, it supports primitive types like string, number and boolean, but we planned to add more advanced scenarios by the time (like navigation/lookup properties).
+
+See the [Module Entity Extensions](https://docs.abp.io/en/commercial/latest/guides/module-entity-extensions) guide to learn how to use it and configure details.
+
+#### Other Extension Points
+
+There are also some other pre-defined points to customize and extend the user interface of a depended module:
+
+* You can add a new action for an entity on the data table (left side on the picture below).
+* You can add new buttons (or other controls) to the page toolbar (right side on the picture below).
+* You can add custom columns to a data table.
+
+
+
+See the [Customizing the Modules](https://docs.abp.io/en/commercial/latest/guides/customizing-modules) guide to learn all the possible ways to customize a depended module.
+
+### Text Template Management Module
+
+We are introducing a new module with the v2.7 release: [Text Template Management](https://docs.abp.io/en/commercial/latest/modules/text-template-management). It is basically used to edit text/email templates (introduced with the ABP Framework 2.7) on the user interface and save changed in the database.
+
+A screenshot from the content editing for the password reset email template:
+
+
+
+This module comes pre-installed when you create a new project.
+
+### Entity History Views
+
+Audit logging UI module now shows all the entity changes in the application with property change details.
+
+
+
+You can also check history for an entity when you click to the actions menu for the entity:
+
+
+
+### More Samples
+
+We are creating more advanced sample applications built with the ABP Commercial. Easy CRM is one of them which will be available in a few days to the commercial customers.
+
+Here, a screenshot from the Easy CRM dashboard:
+
+
+
+It has accounts, contacts, product groups, products, orders and so on.
+
+### New Modules
+
+We continue to improve existing modules and creating new modules. In addition to the new [text template management](https://docs.abp.io/en/commercial/latest/modules/text-template-management) module introduced above;
+
+* We've recently released a [payment module](https://commercial.abp.io/modules/Volo.Payment) that currently works with PayU and 2Checkout payment gateways. More gateways will be added by the time.
+* We've created a simple [Twilio SMS integration](https://docs.abp.io/en/commercial/latest/modules/twilio-sms) module to send SMS over the Twilio.
+* We are working on a **chat module** that is currently being developed and will be available in the next weeks.
+* We are working on the **organization unit management** system for the identity module to create hierarchical organization units (domain layer will be open source & free).
+
+More modules, theme and tooling options are being developed for the ABP Commercial and the ABP Framework.
+
+## ABP Framework vs ABP Commercial
+
+We ([Volosoft](https://volosoft.com/) - the core team behind the ABP.IO platform), are spending almost equal time on the ABP Framework and the ABP Commercial and we consider the ABP.IO platform as a whole.
+
+[ABP Framework](https://abp.io/) provides all the infrastructure and application independent framework features to make you more productive, focus on your own business code and implement software development best practices. It provides you a well defined and comfortable development experience without repeating yourself.
+
+[ABP Commercial](https://commercial.abp.io/) provides pre-built functionalities, themes and tooling to save your time if your requirements involve these functionalities in addition to the premium support for the framework and the pre-built modules.
\ No newline at end of file
diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/abp-commercial-ui-extensions.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/abp-commercial-ui-extensions.png
new file mode 100644
index 0000000000..52fe37a712
Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/abp-commercial-ui-extensions.png differ
diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/audit-log-entity-changes.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/audit-log-entity-changes.png
new file mode 100644
index 0000000000..e06b3300ee
Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/audit-log-entity-changes.png differ
diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/easy-crm.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/easy-crm.png
new file mode 100644
index 0000000000..e40f399525
Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/easy-crm.png differ
diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/github-contribution-graph.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/github-contribution-graph.png
new file mode 100644
index 0000000000..ff3db2ad22
Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/github-contribution-graph.png differ
diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/module-entity-extended-ui.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/module-entity-extended-ui.png
new file mode 100644
index 0000000000..25621aabb7
Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/module-entity-extended-ui.png differ
diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/tenant-entity-changes.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/tenant-entity-changes.png
new file mode 100644
index 0000000000..4cd573c588
Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/tenant-entity-changes.png differ
diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/text-template-content-ui.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/text-template-content-ui.png
new file mode 100644
index 0000000000..037e3e4d8b
Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/text-template-content-ui.png differ
diff --git a/docs/en/CLI.md b/docs/en/CLI.md
index b687a5244a..75e3e027b4 100644
--- a/docs/en/CLI.md
+++ b/docs/en/CLI.md
@@ -135,6 +135,8 @@ abp update [options]
* `--include-previews` or `-p`: Includes preview, beta and rc packages while checking the latest versions.
* `--npm`: Only updates NPM packages.
* `--nuget`: Only updates NuGet packages.
+* `--solution-path` or `-sp`: Specify the solution path. Use the current directory by default
+* `--solution-name` or `-sn`: Specify the solution name. Search `*.sln` files in the directory by default.
### switch-to-preview
@@ -170,7 +172,11 @@ Some features of the CLI requires to be logged in to abp.io platform. To login w
abp login
```
-Notice that, a new login with an already active session, will kill the previous session and creates a new one.
+```bash
+abp login -p
+```
+
+Notice that, a new login with an already active session, overwrites the previous session.
### logout
diff --git a/docs/en/Customizing-Application-Modules-Overriding-Services.md b/docs/en/Customizing-Application-Modules-Overriding-Services.md
index 7f0c0502ea..b9890b3aca 100644
--- a/docs/en/Customizing-Application-Modules-Overriding-Services.md
+++ b/docs/en/Customizing-Application-Modules-Overriding-Services.md
@@ -106,30 +106,33 @@ You could completely **re-write** the entire business logic for a user creation
[ExposeServices(typeof(IdentityUserManager))]
public class MyIdentityUserManager : IdentityUserManager
{
- public MyIdentityUserManager(
- IdentityUserStore store,
- IOptions optionsAccessor,
- IPasswordHasher passwordHasher,
- IEnumerable> userValidators,
- IEnumerable> passwordValidators,
- ILookupNormalizer keyNormalizer,
- IdentityErrorDescriber errors,
- IServiceProvider services,
- ILogger logger,
- ICancellationTokenProvider cancellationTokenProvider
- ) : base(
- store,
- optionsAccessor,
- passwordHasher,
- userValidators,
- passwordValidators,
- keyNormalizer,
- errors,
- services,
- logger,
- cancellationTokenProvider)
- {
- }
+ public MyIdentityUserManager(
+ IdentityUserStore store,
+ IIdentityRoleRepository roleRepository,
+ IIdentityUserRepository userRepository,
+ IOptions optionsAccessor,
+ IPasswordHasher passwordHasher,
+ IEnumerable> userValidators,
+ IEnumerable> passwordValidators,
+ ILookupNormalizer keyNormalizer,
+ IdentityErrorDescriber errors,
+ IServiceProvider services,
+ ILogger logger,
+ ICancellationTokenProvider cancellationTokenProvider) :
+ base(store,
+ roleRepository,
+ userRepository,
+ optionsAccessor,
+ passwordHasher,
+ userValidators,
+ passwordValidators,
+ keyNormalizer,
+ errors,
+ services,
+ logger,
+ cancellationTokenProvider)
+ {
+ }
public override async Task CreateAsync(IdentityUser user)
{
diff --git a/docs/en/Distributed-Event-Bus-RabbitMQ-Integration.md b/docs/en/Distributed-Event-Bus-RabbitMQ-Integration.md
new file mode 100644
index 0000000000..dc3d5c172f
--- /dev/null
+++ b/docs/en/Distributed-Event-Bus-RabbitMQ-Integration.md
@@ -0,0 +1,3 @@
+# Distributed Event Bus RabbitMQ Integration
+
+TODO
\ No newline at end of file
diff --git a/docs/en/Entity-Framework-Core-Migrations.md b/docs/en/Entity-Framework-Core-Migrations.md
index e0772579ec..00e791a94e 100644
--- a/docs/en/Entity-Framework-Core-Migrations.md
+++ b/docs/en/Entity-Framework-Core-Migrations.md
@@ -546,6 +546,8 @@ Entity extension system solves the main problem of the extra properties: It can
All you need to do is to use the `ObjectExtensionManager` to define the extra property as explained above, in the `AppRole` example. Then you can continue to use the same `GetProperty` and `SetProperty` methods defined above to get/set the related property on the entity, but this time stored as a separate field in the database.
+See the [entity extension system](Customizing-Application-Modules-Extending-Entities.md) for details.
+
###### 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.
diff --git a/docs/en/Entity-Framework-Core-PostgreSQL.md b/docs/en/Entity-Framework-Core-PostgreSQL.md
index 328ca4d416..4c6099b809 100644
--- a/docs/en/Entity-Framework-Core-PostgreSQL.md
+++ b/docs/en/Entity-Framework-Core-PostgreSQL.md
@@ -10,9 +10,9 @@ This document explains how to switch to the **PostgreSQL** database provider for
Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, remove `typeof(AbpEntityFrameworkCoreSqlServerModule)` from the `DependsOn` attribute, add `typeof(AbpEntityFrameworkCorePostgreSqlModule)` (also replace `using Volo.Abp.EntityFrameworkCore.SqlServer;` with `using Volo.Abp.EntityFrameworkCore.PostgreSql;`).
-## UsePostgreSql()
+## UseNpgsql()
-Find `UseSqlServer()` call in *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project and replace with `UsePostgreSql()`.
+Find `UseSqlServer()` call in *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project and replace with `UseNpgsql()`.
Find `UseSqlServer()` call in *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project and replace with `UseNpgsql()`.
diff --git a/docs/en/Getting-Started-AspNetCore-Application.md b/docs/en/Getting-Started-AspNetCore-Application.md
index b78ce1b9cf..9698b1dfb0 100644
--- a/docs/en/Getting-Started-AspNetCore-Application.md
+++ b/docs/en/Getting-Started-AspNetCore-Application.md
@@ -57,7 +57,7 @@ namespace BasicAspNetCoreApplication
app.UseStaticFiles();
app.UseRouting();
- app.UseMvcWithDefaultRouteAndArea();
+ app.UseConfiguredEndpoints();
}
}
}
diff --git a/docs/en/Getting-Started.md b/docs/en/Getting-Started.md
index 07e55f553e..fa7f47f276 100644
--- a/docs/en/Getting-Started.md
+++ b/docs/en/Getting-Started.md
@@ -25,6 +25,12 @@ The following tools should be installed on your development machine:
* [Node v12+](https://nodejs.org)
* [Yarn v1.19+](https://classic.yarnpkg.com/)
+{{ if Tiered == "Yes" }}
+
+* [Redis](https://redis.io/): The applications use Redis as as [distributed cache](../Caching.md). So, you need to have Redis installed & running.
+
+{{ end }}
+
> You can use another editor instead of Visual Studio as long as it supports .NET Core and ASP.NET Core.
@@ -400,8 +406,8 @@ Enter **admin** as the username and **1q2w3E*** as the password to login to the
The application is up and running. You can continue to develop your application based on this startup template.
-> The [application startup template](startup-templates/application/index.md) includes the TenantManagement and Identity modules.
+> The [application startup template](Startup-Templates/Application.md) includes the TenantManagement and Identity modules.
## What's next?
-[Application development tutorial](tutorials/book-store/part-1.md)
+[Application development tutorial](Tutorials/Part-1.md)
diff --git a/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md b/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md
index 10b1e05a4a..434d5f8bac 100644
--- a/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md
+++ b/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md
@@ -158,6 +158,17 @@ You can find the source code of the completed example [here](https://github.com/
````
+* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error!
+
+
+ * This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add
+
+ ````csharp
+ options.Scope.Add("email");
+ ````
+
+ to your openid configuration.
+
* Help! I keep getting ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** error!
* If you set your **CallbackPath** in appsettings as:
@@ -169,18 +180,19 @@ You can find the source code of the completed example [here](https://github.com/
}
````
- your **Redirect URI** of your application in azure portal must be with domain like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`.
+ your **Redirect URI** of your application in azure portal must be with domain like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`.
+
+* Help! I keep getting ***AADSTS700051: The response_type 'token' is not enabled for the application.*** error!
-* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error!
+ * This error occurs when you request **token** (access token) along with **id_token** without enabling Access tokens on Azure portal app registrations. Simply tick **Access tokens** checkbox located on top of ID tokens to be able to request token aswell.
+* Help! I keep getting ***AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret*** error!
- * This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add
-
- ````csharp
- options.Scope.Add("email");
- ````
-
- to your openid configuration.
+ * This error occurs when you request **code** along with **id_token**. You need to add **client secret** on azure portal app registrations, under **Certificates & secrets** menu. Afterwards, you need to add openid configuration option like:
+
+ ````csharp
+ options.ClientSecret = "Value of your secret on azure portal";
+ ````
* How can I **debug/watch** which claims I get before they get mapped?
@@ -188,13 +200,12 @@ You can find the source code of the completed example [here](https://github.com/
````csharp
options.Events.OnTokenValidated = (async context =>
- {
- var claimsFromOidcProvider = context.Principal.Claims.ToList();
- await Task.CompletedTask;
- });
+ {
+ var claimsFromOidcProvider = context.Principal.Claims.ToList();
+ await Task.CompletedTask;
+ });
````
-
## See Also
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md).
diff --git a/docs/en/Localization.md b/docs/en/Localization.md
index 11b838cf7f..954fe8620f 100644
--- a/docs/en/Localization.md
+++ b/docs/en/Localization.md
@@ -87,6 +87,21 @@ A JSON localization file content is shown below:
* Every localization file should define the `culture` code for the file (like "en" or "en-US").
* `texts` section just contains key-value collection of the localization strings (keys may have spaces too).
+### Default Resource
+
+`AbpLocalizationOptions.DefaultResourceType` can be set to a resource type, so it is used when the localization resource was not specified:
+
+````csharp
+Configure(options =>
+{
+ options.DefaultResourceType = typeof(TestResource);
+});
+````
+
+> The [application startup template](Startup-Templates/Application.md) sets `DefaultResourceType` to the localization resource of the application.
+
+See the *Client Side* section below for a use case.
+
### Short Localization Resource Name
Localization resources are also available in the client (JavaScript) side. So, setting a short name for the localization resource makes it easy to use localization texts. Example:
@@ -166,6 +181,10 @@ public class MyService
}
````
+##### Format Arguments
+
+Format arguments can be passed after the localization key. If your message is `Hello {0}, welcome!`, then you can pass the `{0}` argument to the localizer like `_localizer["HelloMessage", "John"]`
+
#### Simplest Usage In A Razor View/Page
````c#
@@ -180,18 +199,47 @@ Refer to the [Microsoft's localization documentation](https://docs.microsoft.com
ABP provides JavaScript services to use the same localized texts in the client side.
-Get a localization resource:
+#### getResource
+
+`abp.localization.getResource` function is used to get a localization resource:
````js
var testResource = abp.localization.getResource('Test');
````
-Localize a string:
+Then you can localize a string based on this resource:
````js
var str = testResource('HelloWorld');
````
+#### localize
+
+`abp.localization.localize` function is a shortcut where you can both specify the text name and the resource name:
+
+````js
+var str = abp.localization.localize('HelloWorld', 'Test');
+````
+
+`HelloWorld` is the text to localize, where `Test` is the localization resource name here.
+
+If you don't specify the localization resource name, it uses the default localization resource defined on the `AbpLocalizationOptions` (see the *Default Resource* section above). Example:
+
+````js
+var str = abp.localization.localize('HelloWorld'); //uses the default resource
+````
+
+##### Format Arguments
+
+If your localized string contains arguments, like `Hello {0}, welcome!`, you can pass arguments to the localization methods. Examples:
+
+````js
+var str1 = abp.localization.getResource('Test')('HelloWelcomeMessage', 'John');
+var str2 = abp.localization.localize('HelloWorld', 'Test', 'John');
+````
+
+Both of the samples above produce the output `Hello John, welcome!`.
+
## See Also
* [Localization in Angular UI](UI/Angular/Localization.md)
\ No newline at end of file
diff --git a/docs/en/Object-Extensions.md b/docs/en/Object-Extensions.md
index 6eff328c85..bee229dc27 100644
--- a/docs/en/Object-Extensions.md
+++ b/docs/en/Object-Extensions.md
@@ -197,8 +197,8 @@ ObjectExtensionManager.Instance
"SocialSecurityNumber",
options =>
{
- options.ValidationAttributes.Add(new RequiredAttribute());
- options.ValidationAttributes.Add(
+ options.Attributes.Add(new RequiredAttribute());
+ options.Attributes.Add(
new StringLengthAttribute(32) {
MinimumLength = 6
}
@@ -248,12 +248,12 @@ ObjectExtensionManager.Instance
objConfig.AddOrUpdateProperty("Password", propertyConfig =>
{
- propertyConfig.ValidationAttributes.Add(new RequiredAttribute());
+ propertyConfig.Attributes.Add(new RequiredAttribute());
});
objConfig.AddOrUpdateProperty("PasswordRepeat", propertyConfig =>
{
- propertyConfig.ValidationAttributes.Add(new RequiredAttribute());
+ propertyConfig.Attributes.Add(new RequiredAttribute());
});
//Write a common validation logic works on multiple properties
diff --git a/docs/en/Samples/Index.md b/docs/en/Samples/Index.md
new file mode 100644
index 0000000000..5f46b0f42e
--- /dev/null
+++ b/docs/en/Samples/Index.md
@@ -0,0 +1,62 @@
+# Sample Applications
+
+Here, a list of official samples built with the ABP Framework. Most of these samples are located under the [abpframework/abp-samples](https://github.com/abpframework/abp-samples) GitHub repository.
+
+### Microservice Demo
+
+A complete solution to demonstrate how to build systems based on the microservice architecture.
+
+* [The complete documentation for this sample](Microservice-Demo.md)
+* [Source code](https://github.com/abpframework/abp/tree/dev/samples/MicroserviceDemo)
+* [Microservice architecture document](../Microservice-Architecture.md)
+
+### Book Store
+
+A simple CRUD application to show basic principles of developing an application with the ABP Framework. The same sample was implemented with different technologies:
+
+* **Book Store: Razor Pages UI & Entity Framework Core**
+
+ * [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC)
+ * [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore)
+
+* **Book Store: Angular UI & MongoDB**
+
+ * [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG)
+ * [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
+
+* **Book Store: Modular application (Razor Pages UI & EF Core)**
+
+ * [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Modular)
+
+While there is no Razor Pages & MongoDB combination, you can check both documents to understand it since DB & UI selection don't effect each other.
+
+### Other Samples
+
+* **Entity Framework Migrations**: A solution to demonstrate how to split your application into multiple databases each database contains different modules.
+ * [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo)
+ * [EF Core database migrations document](../Entity-Framework-Core-Migrations.md)
+* **SignalR Demo**: A simple chat application that allows to send and receive messages among authenticated users.
+ * [Source code](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo)
+ * [SignalR Integration document](../SignalR-Integration.md)
+* **Dashboard Demo**: A simple application to show how to use the widget system for the ASP.NET Core MVC UI.
+ * [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo)
+ * [Widget documentation](../UI/AspNetCore/Widgets.md)
+* **RabbitMQ Event Bus Demo**: A solution consists of two applications communicating to each other via distributed events with RabbitMQ integration.
+ * [Source code](https://github.com/abpframework/abp-samples/tree/master/RabbitMqEventBus)
+ * [Distributed event bus document](../Distributed-Event-Bus.md)
+ * [RabbitMQ distributed event bus integration document](../Distributed-Event-Bus-RabbitMQ-Integration.md)
+* **Text Templates Demo**: Shows different use cases of the text templating system.
+ * [Source code](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo)
+ * [Text templating documentation](../Text-Templating.md)
+* **Authentication Customization**: A solution to show how to customize the authentication for ASP.NET Core MVC / Razor Pages applications.
+ * [Source code](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization)
+ * Related "[How To](../How-To/Index.md)" documents:
+ * [Azure Active Directory Authentication](../How-To/Azure-Active-Directory-Authentication-MVC.md)
+ * [Customize the Login Page](../How-To/Customize-Login-Page-MVC.md)
+ * [Customize the SignIn Manager](../How-To/Customize-SignIn-Manager.md)
+* **Empty ASP.NET Core Application**: The most basic ASP.NET Core application with the ABP Framework installed.
+ * [Source code](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication)
+ * [Documentation](../Getting-Started-AspNetCore-Application.md)
+* **Empty Console Application**: The most basic console application with the ABP Framework installed.
+ * [Source code](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication)
+ * [Documentation](../Getting-Started-Console-Application.md)
\ No newline at end of file
diff --git a/docs/en/Samples/Microservice-Demo.md b/docs/en/Samples/Microservice-Demo.md
index 567d043ef9..25eaf68055 100644
--- a/docs/en/Samples/Microservice-Demo.md
+++ b/docs/en/Samples/Microservice-Demo.md
@@ -552,7 +552,7 @@ app.MapWhen(ctx => ctx.Request.Path.ToString().StartsWith("/api/abp/") ||
ctx.Request.Path.ToString().StartsWith("/Abp/"),
app2 =>
{
- app2.UseMvcWithDefaultRouteAndArea();
+ app2.UseConfiguredEndpoints();
});
app.UseOcelot().Wait();
diff --git a/docs/en/SignalR-Integration.md b/docs/en/SignalR-Integration.md
new file mode 100644
index 0000000000..0a2ebdaa54
--- /dev/null
+++ b/docs/en/SignalR-Integration.md
@@ -0,0 +1,227 @@
+# SignalR Integration
+
+> It is already possible to follow [the standard Microsoft tutorial](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr) to add [SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) to your application. However, ABP provides a SignalR integration packages that simplify the integration and usage.
+
+## Installation
+
+### Server Side
+
+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 your project (.csproj file) and type the following command:
+
+```bash
+abp add-package Volo.Abp.AspNetCore.SignalR
+```
+
+> You typically want to add this package to the web or API layer of your application, depending on your architecture.
+
+#### Manual Installation
+
+If you want to manually install;
+
+1. Add the [Volo.Abp.AspNetCore.SignalR](https://www.nuget.org/packages/Volo.Abp.AspNetCore.SignalR) NuGet package to your project:
+
+ ```
+ Install-Package Volo.Abp.AspNetCore.SignalR
+ ```
+
+ Or use the Visual Studio NuGet package management UI to install it.
+
+2. Add the `AbpAspNetCoreSignalRModule` to the dependency list of your module:
+
+```csharp
+[DependsOn(
+ //...other dependencies
+ typeof(AbpAspNetCoreSignalRModule) //Add the new module dependency
+ )]
+public class YourModule : AbpModule
+{
+}
+```
+
+> You don't need to use the `services.AddSignalR()` and the `app.UseEndpoints(...)`, it's done by the `AbpAspNetCoreSignalRModule`.
+
+### Client Side
+
+Client side installation depends on your UI framework / client type.
+
+#### ASP.NET Core MVC / Razor Pages UI
+
+Run the following command in the root folder of your web project:
+
+````bash
+yarn add @abp/signalr
+````
+
+> This requires to [install yarn](https://yarnpkg.com/) if you haven't install before.
+
+This will add the `@abp/signalr` to the dependencies in the `package.json` of your project:
+
+````json
+{
+ ...
+ "dependencies": {
+ ...
+ "@abp/signalr": "~2.7.0"
+ }
+}
+````
+
+Run the `gulp` in the root folder of your web project:
+
+````bash
+gulp
+````
+
+This will copy the SignalR JavaScript files into your project:
+
+
+
+Finally, add the following code to your page/view to include the `signalr.js` file
+
+````xml
+@section scripts {
+
+}
+````
+
+It requires to add `@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR` to your page/view.
+
+> You could add the `signalr.js` file in a standard way. But using the `SignalRBrowserScriptContributor` has additional benefits. See the [Client Side Package Management](UI/AspNetCore/Client-Side-Package-Management.md) and [Bundling & Minification](UI/AspNetCore/Bundling-Minification.md) documents for details.
+
+That's all. you can use the [SignalR JavaScript API](https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client) in your page.
+
+#### Other UI Frameworks / Clients
+
+Please refer to [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) for other type of clients.
+
+## The ABP Framework Integration
+
+This section covers the additional benefits when you use the ABP Framework integration packages.
+
+### Hub Route & Mapping
+
+ABP automatically registers all the hubs to the [dependency injection](Dependency-Injection.md) (as transient) and maps the hub endpoint. So, you don't have to use the ` app.UseEndpoints(...)` to map your hubs. Hub route (URL) is determined conventionally based on your hub name.
+
+Example:
+
+````csharp
+public class MessagingHub : Hub
+{
+ //...
+}
+````
+
+The hub route will be `/signalr-hubs/messasing` for the `MessasingHub`:
+
+* Adding a standard `/signalr-hubs/` prefix
+* Continue with the **camel case** hub name, without the `Hub` suffix.
+
+If you want to specify the route, you can use the `HubRoute` attribute:
+
+````csharp
+[HubRoute("/my-messasing-hub")]
+public class MessagingHub : Hub
+{
+ //...
+}
+````
+
+### AbpHub Base Classes
+
+Instead of the standard `Hub` and `Hub` classes, you can inherit from the `AbpHub` or `AbpHub` which hve useful base properties like `CurrentUser`.
+
+Example:
+
+````csharp
+public class MessagingHub : AbpHub
+{
+ public async Task SendMessage(string targetUserName, string message)
+ {
+ var currentUserName = CurrentUser.UserName; //Access to the current user info
+ var txt = L["MyText"]; //Localization
+ }
+}
+````
+
+> While you could inject the same properties into your hub constructor, this way simplifies your hub class.
+
+### Manual Registration / Mapping
+
+ABP automatically registers all the hubs to the [dependency injection](Dependency-Injection.md) as a **transient service**. If you want to **disable auto dependency injection** registration for your hub class, just add a `DisableConventionalRegistration` attribute. You can still register your hub class to dependency injection in the `ConfigureServices` method of your module if you like:
+
+````csharp
+context.Services.AddTransient();
+````
+
+When **you or ABP** register the class to the dependency injection, it is automatically mapped to the endpoint route configuration just as described in the previous sections. You can use `DisableAutoHubMap` attribute if you want to manually map your hub class.
+
+For manual mapping, you have two options:
+
+1. Use the `AbpSignalROptions` to add your map configuration (in the `ConfigureServices` method of your [module](Module-Development-Basics.md)), so ABP still performs the endpoint mapping for your hub:
+
+````csharp
+Configure(options =>
+{
+ options.Hubs.Add(
+ new HubConfig(
+ typeof(MessagingHub), //Hub type
+ "/my-messaging/route", //Hub route (URL)
+ hubOptions =>
+ {
+ //Additional options
+ hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30);
+ }
+ )
+ );
+});
+````
+
+This is a good way to provide additional SignalR options.
+
+If you don't want to disable auto hub map, but still want to perform additional SignalR configuration, use the `options.Hubs.AddOrUpdate(...)` method:
+
+````csharp
+Configure(options =>
+{
+ options.Hubs.AddOrUpdate(
+ typeof(MessagingHub), //Hub type
+ config => //Additional configuration
+ {
+ config.RoutePattern = "/my-messaging-hub"; //override the default route
+ config.ConfigureActions.Add(hubOptions =>
+ {
+ //Additional options
+ hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30);
+ });
+ }
+ );
+});
+````
+
+This is the way you can modify the options of a hub class defined in a depended module (where you don't have the source code access).
+
+2. Change `app.UseConfiguredEndpoints` in the `OnApplicationInitialization` method of your [module](Module-Development-Basics.md) as shown below (added a lambda method as the parameter).
+
+````csharp
+app.UseConfiguredEndpoints(endpoints =>
+{
+ endpoints.MapHub("/my-messaging-hub", options =>
+ {
+ options.LongPolling.PollTimeout = TimeSpan.FromSeconds(30);
+ });
+});
+````
+
+### UserIdProvider
+
+ABP implements SignalR's `IUserIdProvider` interface to provide the current user id from the `ICurrentUser` service of the ABP framework (see [the current user service](CurrentUser.md)), so it will be integrated to the authentication system of your application. The implementing class is the `AbpSignalRUserIdProvider`, if you want to change/override it.
+
+## Example Application
+
+See the [SignalR Integration Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) as a sample application. It has a simple Chat page to send messages between (authenticated) users.
+
+
\ No newline at end of file
diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md
new file mode 100644
index 0000000000..18a360c5c8
--- /dev/null
+++ b/docs/en/Text-Templating.md
@@ -0,0 +1,455 @@
+# Text Templating
+
+## Introduction
+
+ABP Framework provides a simple, yet efficient text template system. Text templating is used to dynamically render contents based on a template and a model (a data object):
+
+***TEMPLATE + MODEL ==render==> RENDERED CONTENT***
+
+It is very similar to an ASP.NET Core Razor View (or Page):
+
+*RAZOR VIEW (or PAGE) + MODEL ==render==> HTML CONTENT*
+
+You can use the rendered output for any purpose, like sending emails or preparing some reports.
+
+### Example
+
+Here, a simple template:
+
+````
+Hello {%{{{model.name}}}%} :)
+````
+
+You can define a class with a `Name` property to render this template:
+
+````csharp
+public class HelloModel
+{
+ public string Name { get; set; }
+}
+````
+
+If you render the template with a `HelloModel` with the `Name` is `John`, the rendered output is will be:
+
+````
+Hello John :)
+````
+
+Template rendering engine is very powerful;
+
+* It is based on the [Scriban](https://github.com/lunet-io/scriban) library, so it supports **conditional logics**, **loops** and much more.
+* Template content **can be localized**.
+* You can define **layout templates** to be used as the layout while rendering other templates.
+* You can pass arbitrary objects to the template context (beside the model) for advanced scenarios.
+
+### Source Code
+
+Get [the source code of the sample application](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) developed and referred through this document.
+
+## 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.TextTemplating
+````
+
+### Manual Installation
+
+If you want to manually install;
+
+1. Add the [Volo.Abp.TextTemplating](https://www.nuget.org/packages/Volo.Abp.TextTemplating) NuGet package to your project:
+
+````
+Install-Package Volo.Abp.TextTemplating
+````
+
+2. Add the `AbpTextTemplatingModule` to the dependency list of your module:
+
+````csharp
+[DependsOn(
+ //...other dependencies
+ typeof(AbpTextTemplatingModule) //Add the new module dependency
+ )]
+public class YourModule : AbpModule
+{
+}
+````
+
+## Defining Templates
+
+Before rendering a template, you should define it. Create a class inheriting from the `TemplateDefinitionProvider` base class:
+
+````csharp
+public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider
+{
+ public override void Define(ITemplateDefinitionContext context)
+ {
+ context.Add(
+ new TemplateDefinition("Hello") //template name: "Hello"
+ .WithVirtualFilePath(
+ "/Demos/Hello/Hello.tpl", //template content path
+ isInlineLocalized: true
+ )
+ );
+ }
+}
+````
+
+* `context` object is used to add new templates or get the templates defined by depended modules. Used `context.Add(...)` to define a new template.
+* `TemplateDefinition` is the class represents a template. Each template must have a unique name (that will be used while you are rendering the template).
+* `/Demos/Hello/Hello.tpl` is the path of the template file.
+* `isInlineLocalized` is used to declare if you are using a single template for all languages (`true`) or different templates for each language (`false`). See the Localization section below for more.
+
+### The Template Content
+
+`WithVirtualFilePath` indicates that we are using the [Virtual File System](Virtual-File-System.md) to store the template content. Create a `Hello.tpl` file inside your project and mark it as "**embedded resource**" on the properties window:
+
+
+
+Example `Hello.tpl` content is shown below:
+
+````
+Hello {%{{{model.name}}}%} :)
+````
+
+The [Virtual File System](Virtual-File-System.md) requires to add your files in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class:
+
+````csharp
+Configure(options =>
+{
+ options.FileSets.AddEmbedded("TextTemplateDemo");
+});
+````
+
+* `TextTemplateDemoModule` is the module class that you define your template in.
+* `TextTemplateDemo` is the root namespace of your project.
+
+## Rendering the Template
+
+`ITemplateRenderer` service is used to render a template content.
+
+### Example: Rendering a Simple Template
+
+````csharp
+public class HelloDemo : ITransientDependency
+{
+ private readonly ITemplateRenderer _templateRenderer;
+
+ public HelloDemo(ITemplateRenderer templateRenderer)
+ {
+ _templateRenderer = templateRenderer;
+ }
+
+ public async Task RunAsync()
+ {
+ var result = await _templateRenderer.RenderAsync(
+ "Hello", //the template name
+ new HelloModel
+ {
+ Name = "John"
+ }
+ );
+
+ Console.WriteLine(result);
+ }
+}
+````
+
+* `HelloDemo` is a simple class that injects the `ITemplateRenderer` in its constructor and uses it inside the `RunAsync` method.
+* `RenderAsync` gets two fundamental parameters:
+ * `templateName`: The name of the template to be rendered (`Hello` in this example).
+ * `model`: An object that is used as the `model` inside the template (a `HelloModel` object in this example).
+
+The result shown below for this example:
+
+````csharp
+Hello John :)
+````
+
+### Anonymous Model
+
+While it is suggested to create model classes for the templates, it would be practical (and possible) to use anonymous objects for simple cases:
+
+````csharp
+var result = await _templateRenderer.RenderAsync(
+ "Hello",
+ new
+ {
+ Name = "John"
+ }
+);
+````
+
+In this case, we haven't created a model class, but created an anonymous object as the model.
+
+### PascalCase vs camelCase
+
+PascalCase property names (like `UserName`) is used as camelCase (like `userName`) in the templates.
+
+## Localization
+
+It is possible to localize a template content based on the current culture. There are two types of localization options described in the following sections.
+
+### Inline localization
+
+Inline localization uses the [localization system](Localization.md) to localize texts inside templates.
+
+#### Example: Reset Password Link
+
+Assuming you need to send an email to a user to reset her/his password. Here, the template content:
+
+````
+{%{{{L "ResetMyPassword"}}}%}
+````
+
+`L` function is used to localize the given key based on the current user culture. You need to define the `ResetMyPassword` key inside your localization file:
+
+````json
+"ResetMyPassword": "Click here to reset your password"
+````
+
+You also need to declare the localization resource to be used with this template, inside your template definition provider class:
+
+````csharp
+context.Add(
+ new TemplateDefinition(
+ "PasswordReset", //Template name
+ typeof(DemoResource) //LOCALIZATION RESOURCE
+ ).WithVirtualFilePath(
+ "/Demos/PasswordReset/PasswordReset.tpl", //template content path
+ isInlineLocalized: true
+ )
+);
+````
+
+That's all. When you render this template like that:
+
+````csharp
+var result = await _templateRenderer.RenderAsync(
+ "PasswordReset", //the template name
+ new PasswordResetModel
+ {
+ Link = "https://abp.io/example-link?userId=123&token=ABC"
+ }
+);
+````
+
+You will see the localized result:
+
+````csharp
+Click here to reset your password
+````
+
+> If you define the [default localization resource](Localization.md) for your application, then no need to declare the resource type for the template definition.
+
+### Multiple Contents Localization
+
+Instead of a single template that uses the localization system to localize the template, you may want to create different template files for each language. It can be needed if the template should be completely different for a specific culture rather than simple text localizations.
+
+#### Example: Welcome Email Template
+
+Assuming that you want to send a welcome email to your users, but want to define a completely different template based on the user culture.
+
+First, create a folder and put your templates inside it, like `en.tpl`, `tr.tpl`... one for each culture you support:
+
+
+
+Then add your template definition in the template definition provider class:
+
+````csharp
+context.Add(
+ new TemplateDefinition(
+ name: "WelcomeEmail",
+ defaultCultureName: "en"
+ )
+ .WithVirtualFilePath(
+ "/Demos/WelcomeEmail/Templates", //template content folder
+ isInlineLocalized: false
+ )
+);
+````
+
+* Set **default culture name**, so it fallbacks to the default culture if there is no template for the desired culture.
+* Specify **the template folder** rather than a single template file.
+* Set `isInlineLocalized` to `false` for this case.
+
+That's all, you can render the template for the current culture:
+
+````csharp
+var result = await _templateRenderer.RenderAsync("WelcomeEmail");
+````
+
+> Skipped the modal for this example to keep it simple, but you can use models as just explained before.
+
+### Specify the Culture
+
+`ITemplateRenderer` service uses the current culture (`CultureInfo.CurrentUICulture`) if not specified. If you need, you can specify the culture as the `cultureName` parameter:
+
+````csharp
+var result = await _templateRenderer.RenderAsync(
+ "WelcomeEmail",
+ cultureName: "en"
+);
+````
+
+## Layout Templates
+
+Layout templates are used to create shared layouts among other templates. It is similar to the layout system in the ASP.NET Core MVC / Razor Pages.
+
+### Example: Email HTML Layout Template
+
+For example, you may want to create a single layout for all of your email templates.
+
+First, create a template file just like before:
+
+````xml
+
+
+
+
+
+
+ {%{{{content}}}%}
+
+
+````
+
+* A layout template must have a **{%{{{content}}}%}** part as a place holder for the rendered child content.
+
+The register your template in the template definition provider:
+
+````csharp
+context.Add(
+ new TemplateDefinition(
+ "EmailLayout",
+ isLayout: true //SET isLayout!
+ ).WithVirtualFilePath(
+ "/Demos/EmailLayout/EmailLayout.tpl",
+ isInlineLocalized: true
+ )
+);
+````
+
+Now, you can use this template as the layout of any other template:
+
+````csharp
+context.Add(
+ new TemplateDefinition(
+ name: "WelcomeEmail",
+ defaultCultureName: "en",
+ layout: "EmailLayout" //Set the LAYOUT
+ ).WithVirtualFilePath(
+ "/Demos/WelcomeEmail/Templates",
+ isInlineLocalized: false
+ )
+);
+````
+
+## Global Context
+
+ABP passes the `model` that can be used to access to the model inside the template. You can pass more global variables if you need.
+
+An example template content:
+
+````
+A global object value: {%{{{myGlobalObject}}}%}
+````
+
+This template assumes that that is a `myGlobalObject` object in the template rendering context. You can provide it like shown below:
+
+````csharp
+var result = await _templateRenderer.RenderAsync(
+ "GlobalContextUsage",
+ globalContext: new Dictionary
+ {
+ {"myGlobalObject", "TEST VALUE"}
+ }
+);
+````
+
+The rendering result will be:
+
+````
+A global object value: TEST VALUE
+````
+
+## Advanced Features
+
+This section covers some internals and more advanced usages of the text templating system.
+
+### Template Content Provider
+
+`ITemplateRenderer` is used to render the template, which is what you want for most of the cases. However, you can use the `ITemplateContentProvider` to get the raw (not rendered) template contents.
+
+> `ITemplateContentProvider` is internally used by the `ITemplateRenderer` to get the raw template contents.
+
+Example:
+
+````csharp
+public class TemplateContentDemo : ITransientDependency
+{
+ private readonly ITemplateContentProvider _templateContentProvider;
+
+ public TemplateContentDemo(ITemplateContentProvider templateContentProvider)
+ {
+ _templateContentProvider = templateContentProvider;
+ }
+
+ public async Task RunAsync()
+ {
+ var result = await _templateContentProvider
+ .GetContentOrNullAsync("Hello");
+
+ Console.WriteLine(result);
+ }
+}
+````
+
+The result will be the raw template content:
+
+````
+Hello {%{{{model.name}}}%} :)
+````
+
+* `GetContentOrNullAsync` returns `null` if no content defined for the requested template.
+* It can get a `cultureName` parameter that is used if template has different files for different cultures (see Multiple Contents Localization section above).
+
+### Template Content Contributor
+
+`ITemplateContentProvider` service uses `ITemplateContentContributor` implementations to find template contents. There is a single pre-implemented content contributor, `VirtualFileTemplateContentContributor`, which gets template contents from the virtual file system as described above.
+
+You can implement the `ITemplateContentContributor` to read raw template contents from another source.
+
+Example:
+
+````csharp
+public class MyTemplateContentProvider
+ : ITemplateContentContributor, ITransientDependency
+{
+ public async Task GetOrNullAsync(TemplateContentContributorContext context)
+ {
+ var templateName = context.TemplateDefinition.Name;
+
+ //TODO: Try to find content from another source
+ return null;
+ }
+}
+
+````
+
+Return `null` if your source can not find the content, so `ITemplateContentProvider` fallbacks to the next contributor.
+
+### Template Definition Manager
+
+`ITemplateDefinitionManager` service can be used to get the template definitions (created by the template definition providers).
+
+## See Also
+
+* [The source code of the sample application](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) developed and referred through this document.
+* [Localization system](Localization.md).
+* [Virtual File System](Virtual-File-System.md).
\ No newline at end of file
diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md
index a0aa6612e9..be812cc162 100644
--- a/docs/en/Tutorials/Part-1.md
+++ b/docs/en/Tutorials/Part-1.md
@@ -38,7 +38,7 @@ Create a new project named `Acme.BookStore` where `Acme` is the company name and
#### Create the project
-By running the below command, it creates a new ABP 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.
+By running the below command, it creates a new ABP project with the database provider `{{DB_Text}}` and UI option `{{UI_Value}}`. 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}} --mobile none
@@ -61,7 +61,7 @@ After creating the project, you need to apply the initial migrations and create
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
+Further information, see the [running the application section](../Getting-Started?UI={{UI}}#run-the-application).

@@ -335,7 +335,7 @@ INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
### 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:
+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.
@@ -756,28 +756,28 @@ Open a new command line interface (terminal window) and go to your `angular` fol
yarn
```
-#### BooksModule
+#### BookModule
-Run the following command line to create a new module, named `BooksModule`:
+Run the following command line to create a new module, named `BookModule`:
```bash
-yarn ng generate module books --route books --module app.module
+yarn ng generate module book --routing true
```
-
+
#### Routing
-Open the `app-routing.module.ts` file in `src\app` folder. Add the new `import` and replace `books` path as shown below
+Open the `app-routing.module.ts` file in `src\app` folder. Add the new `import` and add a route as shown below
```js
import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <==
-//...replaced original books path with the below
+//...added books path with the below to the routes array
{
path: 'books',
component: ApplicationLayoutComponent,
- loadChildren: () => import('./books/books.module').then(m => m.BooksModule),
+ loadChildren: () => import('./book/book.module').then(m => m.BookModule),
data: {
routes: {
name: '::Menu:Books',
@@ -789,71 +789,50 @@ import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added th
* 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:
+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
+yarn ng generate component book/book-list
```

-Open `books.module.ts` file in the `app\books` folder and replace the content as below:
+Open `book.module.ts` file in the `app\book` 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 { BookRoutingModule } from './book-routing.module';
import { BookListComponent } from './book-list/book-list.component';
import { SharedModule } from '../shared/shared.module'; //<== added this line ==>
@NgModule({
- declarations: [BooksComponent, BookListComponent],
+ declarations: [BookListComponent],
imports: [
CommonModule,
- BooksRoutingModule,
+ BookRoutingModule,
SharedModule, //<== added this line ==>
- ]
+ ],
})
-export class BooksModule { }
+export class BookModule {}
```
* 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:
+Open `book-routing.module.ts` file in the `app\book` folder and replace the content as below:
```js
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
+import { BookListComponent } from './book-list/book-list.component'; // <== added this line ==>
-import { BooksComponent } from './books.component';
-import { BookListComponent } from './book-list/book-list.component'; //<== added this line ==>
-
-//<== replaced routes ==>
+// <== replaced routes ==>
const routes: Routes = [
{
path: '',
- component: BooksComponent,
- children: [{ path: '', component: BookListComponent }],
+ component: BookListComponent,
},
];
@@ -861,169 +840,137 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
-export class BooksRoutingModule { }
+export class BookRoutingModule { }
```
* We imported `BookListComponent` and replaced `routes` const.
-We'll see **book-list works!** text on the books page:
+Run `yarn start` and wait for Angular to serve the application:
+
+```bash
+yarn start
+```
+
+Open the browser and navigate to http://localhost:4200/books. We'll see **book-list works!** text on the books page:

-#### Create BooksState
+#### Create BookState
Run the following command in the terminal to create a new state, named `BooksState`:
-
-
```bash
-yarn ng generate ngxs-schematic:state books
+npx @ngxs/cli --name book --directory src/app/book
```
-* This command creates several new files and updates `app.modules.ts` file to import the `NgxsModule` with the new state.
+* This command creates `book.state.ts` and `book.actions.ts` files in the `src/app/book/state` folder. See the [NGXS CLI documentation](https://www.ngxs.io/plugins/cli).
-#### 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:
+Import the `BookState` to the `app.module.ts` in the `src/app` folder and then add the `BookState` to `forRoot` static method of `NgxsModule` as an array element of the first parameter of the method.
```js
-export namespace Books {
- export interface State {
- books: Response;
- }
+// ...
+import { BookState } from './books/state/book.state'; //<== imported BookState ==>
- export interface Response {
- items: Book[];
- totalCount: number;
- }
+@NgModule({
+ imports: [
+ // other imports
- export interface Book {
- name: string;
- type: BookType;
- publishDate: string;
- price: number;
- lastModificationTime: string;
- lastModifierId: string;
- creationTime: string;
- creatorId: string;
- id: string;
- }
+ NgxsModule.forRoot([BookState]), //<== added BookState ==>
- export enum BookType {
- Undefined,
- Adventure,
- Biography,
- Dystopia,
- Fantastic,
- Horror,
- Science,
- ScienceFiction,
- Poetry,
- }
-}
+ //other imports
+ ],
+ // ...
+})
+export class AppModule {}
```
-* Added `Book` interface that represents a book object and `BookType` enum which represents a book category.
+#### Generate proxies
-#### BooksService
+ABP CLI provides `generate-proxy` command that generates client proxies for your HTTP APIs to make easy to consume your services from the client side. Before running generate-proxy command, your host must be up and running. See the [CLI documentation](../CLI.md)
-Create a new service, named `BooksService` to perform `HTTP` calls to the server:
+Run the following command in the `angular` folder:
```bash
-yarn ng generate service books/shared/books
+abp generate-proxy --module app
```
-
+
-Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below:
+The generated files looks like 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'
- });
- }
-}
-```
+#### GetBooks Action
-* We added the `get` method to get the list of books by performing an HTTP request to the related endpoint.
+Actions can either be thought of as a command which should trigger something to happen, or as the resulting event of something that has already happened. [See NGXS Actions documentation](https://www.ngxs.io/concepts/actions).
-Open the`books.actions.ts` file in `app\store\actions` folder and replace the content below:
+Open the `book.actions.ts` file in `app/book/state` folder and replace the content below:
```js
export class GetBooks {
- static readonly type = '[Books] Get';
+ static readonly type = '[Book] Get';
}
```
-#### Implement BooksState
+#### Implement BookState
-Open the `books.state.ts` file in `app\store\states` folder and replace the content below:
+Open the `book.state.ts` file in `app/book/state` folder and replace the content below:
```js
+import { PagedResultDto } from '@abp/ng.core';
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 { GetBooks } from './book.actions';
+import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
+import { BookDto } from '../models';
+
+export class BookStateModel {
+ public book: PagedResultDto;
+}
-@State({
- name: 'BooksState',
- defaults: { books: {} } as Books.State,
+@State({
+ name: 'BookState',
+ defaults: { book: {} } as BookStateModel,
})
@Injectable()
-export class BooksState {
+export class BookState {
@Selector()
- static getBooks(state: Books.State) {
- return state.books.items || [];
+ static getBooks(state: BookStateModel) {
+ return state.book.items || [];
}
- constructor(private booksService: BooksService) {}
+ constructor(private bookService: BookService) {}
@Action(GetBooks)
- get(ctx: StateContext) {
- return this.booksService.get().pipe(
- tap(booksResponse => {
+ get(ctx: StateContext) {
+ return this.bookService.getListByInput().pipe(
+ tap((booksResponse) => {
ctx.patchState({
- books: booksResponse,
+ book: booksResponse,
});
- }),
+ })
);
}
}
```
-* We added the `GetBooks` action that retrieves the books data via `BooksService` and patches the state.
+* We added the book property to BookStateModel model.
+* We added the `GetBooks` action that retrieves the book data via `BookService` that generated via ABP CLI 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:
+Open the `book-list.component.ts` file in `app\book\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 { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
-import { Books } from '../../store/models';
-import { GetBooks } from '../../store/actions';
+import { finalize } from 'rxjs/operators';
+import { BookDto, BookType } from '../models';
+import { GetBooks } from '../state/book.actions';
+import { BookState } from '../state/book.state';
@Component({
selector: 'app-book-list',
@@ -1031,14 +978,14 @@ import { GetBooks } from '../../store/actions';
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
- @Select(BooksState.getBooks)
- books$: Observable;
+ @Select(BookState.getBooks)
+ books$: Observable;
- booksType = Books.BookType;
+ booksType = BookType;
loading = false;
- constructor(private store: Store) { }
+ constructor(private store: Store) {}
ngOnInit() {
this.get();
@@ -1046,9 +993,10 @@ export class BookListComponent implements OnInit {
get() {
this.loading = true;
- this.store.dispatch(new GetBooks()).subscribe(() => {
- this.loading = false;
- });
+ this.store
+ .dispatch(new GetBooks())
+ .pipe(finalize(() => (this.loading = false)))
+ .subscribe(() => {});
}
}
```
@@ -1056,7 +1004,7 @@ export class BookListComponent implements OnInit {
* 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:
+Open the `book-list.component.html` file in `app\book\book-list` folder and replace the content as below:
```html
diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md
index 9565724cb8..69f3e5436b 100644
--- a/docs/en/Tutorials/Part-2.md
+++ b/docs/en/Tutorials/Part-2.md
@@ -456,161 +456,76 @@ Run the application and try to delete a 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:
+Open `book.action.ts` in `app\book\state` folder and replace the content as below:
```js
-import { Books } from '../models'; //<== added this line ==>
+import { CreateUpdateBookDto } from '../models'; //<== added this line ==>
export class GetBooks {
- static readonly type = '[Books] Get';
+ static readonly type = '[Book] Get';
}
-//added CreateUpdateBook class
+// added CreateUpdateBook class
export class CreateUpdateBook {
- static readonly type = '[Books] Create Update Book';
- constructor(public payload: Books.CreateUpdateBookInput) { }
+ static readonly type = '[Book] Create Update Book';
+ constructor(public payload: CreateUpdateBookDto) { }
}
```
-* We imported the Books namespace and created the `CreateUpdateBook` action.
+* We imported the `CreateUpdateBookDto` model and created the `CreateUpdateBook` action.
-Open `books.state.ts` file in `app\store\states` and replace the content as below:
+Open `book.state.ts` file in `app\book\state` folder and replace the content as below:
```js
+import { PagedResultDto } from '@abp/ng.core';
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 { GetBooks, CreateUpdateBook } from './book.actions'; // <== added CreateUpdateBook==>
+import { BookService } from '../services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
+import { BookDto } from '../models';
+
+export class BookStateModel {
+ public book: PagedResultDto;
+}
-@State({
- name: 'BooksState',
- defaults: { books: {} } as Books.State,
+@State({
+ name: 'BookState',
+ defaults: { book: {} } as BookStateModel,
})
@Injectable()
-export class BooksState {
+export class BookState {
@Selector()
- static getBooks(state: Books.State) {
- return state.books.items || [];
+ static getBooks(state: BookStateModel) {
+ return state.book.items || [];
}
- constructor(private booksService: BooksService) { }
+ constructor(private bookService: BookService) {}
@Action(GetBooks)
- get(ctx: StateContext) {
- return this.booksService.get().pipe(
- tap(booksResponse => {
+ get(ctx: StateContext) {
+ return this.bookService.getListByInput().pipe(
+ tap((bookResponse) => {
ctx.patchState({
- books: booksResponse,
+ book: bookResponse,
});
- }),
+ })
);
}
- //added CreateUpdateBook action listener
+ // added CreateUpdateBook action listener
@Action(CreateUpdateBook)
- save(ctx: StateContext, action: CreateUpdateBook) {
- return this.booksService.create(action.payload);
+ save(ctx: StateContext, action: CreateUpdateBook) {
+ return this.bookService.createByInput(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`.
+When the `SaveBook` action dispatched, the save method is being executed. It calls `createByInput` method of the `BookService`.
#### Add a modal to BookListComponent
@@ -690,15 +605,16 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the
* `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.ts` file in `books\book-list` folder and replace the content as below:
+Open `book-list.component.ts` file in `app\book\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 { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
-import { Books } from '../../store/models';
-import { GetBooks } from '../../store/actions';
+import { finalize } from 'rxjs/operators';
+import { BookDto, BookType } from '../models';
+import { GetBooks } from '../state/book.actions';
+import { BookState } from '../state/book.state';
@Component({
selector: 'app-book-list',
@@ -706,16 +622,16 @@ import { GetBooks } from '../../store/actions';
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
- @Select(BooksState.getBooks)
- books$: Observable;
+ @Select(BookState.getBooks)
+ books$: Observable;
- booksType = Books.BookType;
+ booksType = BookType;
loading = false;
- isModalOpen = false; //<== added this line ==>
+ isModalOpen = false; // <== added this line ==>
- constructor(private store: Store) { }
+ constructor(private store: Store) {}
ngOnInit() {
this.get();
@@ -723,12 +639,13 @@ export class BookListComponent implements OnInit {
get() {
this.loading = true;
- this.store.dispatch(new GetBooks()).subscribe(() => {
- this.loading = false;
- });
+ this.store
+ .dispatch(new GetBooks())
+ .pipe(finalize(() => (this.loading = false)))
+ .subscribe(() => {});
}
- //added createBook method
+ // added createBook method
createBook() {
this.isModalOpen = true;
}
@@ -745,16 +662,17 @@ You can open your browser and click **New book** button to see the new modal.
[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:
+Open `book-list.component.ts` file in `app\book\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 { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
-import { Books } from '../../store/models';
-import { GetBooks } from '../../store/actions';
-import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added this line ==>
+import { finalize } from 'rxjs/operators';
+import { BookDto, BookType } from '../models';
+import { GetBooks } from '../state/book.actions';
+import { BookState } from '../state/book.state';
+import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==>
@Component({
selector: 'app-book-list',
@@ -762,18 +680,18 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added
styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent implements OnInit {
- @Select(BooksState.getBooks)
- books$: Observable;
+ @Select(BookState.getBooks)
+ books$: Observable;
- booksType = Books.BookType;
+ booksType = BookType;
loading = false;
isModalOpen = false;
- form: FormGroup;
+ form: FormGroup; // <== added this line ==>
- constructor(private store: Store, private fb: FormBuilder) { } //<== added FormBuilder ==>
+ constructor(private store: Store, private fb: FormBuilder) {} // <== added FormBuilder ==>
ngOnInit() {
this.get();
@@ -781,9 +699,10 @@ export class BookListComponent implements OnInit {
get() {
this.loading = true;
- this.store.dispatch(new GetBooks()).subscribe(() => {
- this.loading = false;
- });
+ this.store
+ .dispatch(new GetBooks())
+ .pipe(finalize(() => (this.loading = false)))
+ .subscribe(() => {});
}
createBook() {
@@ -791,7 +710,7 @@ export class BookListComponent implements OnInit {
this.isModalOpen = true;
}
- //added buildForm method
+ // added buildForm method
buildForm() {
this.form = this.fb.group({
name: ['', Validators.required],
@@ -804,6 +723,7 @@ export class BookListComponent implements OnInit {
```
* We imported `FormGroup, FormBuilder and Validators`.
+* We added `form: FormGroup` variable.
* 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`.
@@ -854,61 +774,58 @@ Open `book-list.component.html` in `app\books\book-list` folder and replace `
@NgModule({
- declarations: [BooksComponent, BookListComponent],
+ declarations: [BookListComponent],
imports: [
CommonModule,
- BooksRoutingModule,
+ BookRoutingModule,
SharedModule,
- NgbDatepickerModule //<== added this line ==>
- ]
+ NgbDatepickerModule, //<== added this line ==>
+ ],
})
-export class BooksModule { }
+export class BookModule {}
```
* 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:
+Open `book-list.component.ts` file in `app\book\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 { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
-import { Books } from '../../store/models';
-import { GetBooks } from '../../store/actions';
+import { finalize } from 'rxjs/operators';
+import { BookDto, BookType } from '../models';
+import { GetBooks } from '../state/book.actions';
+import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
-import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==>
+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 ==>
+ providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==>
})
export class BookListComponent implements OnInit {
- @Select(BooksState.getBooks)
- books$: Observable;
+ @Select(BookState.getBooks)
+ books$: Observable;
- booksType = Books.BookType;
+ booksType = BookType;
//added bookTypeArr array
- bookTypeArr = Object.keys(Books.BookType).filter(
- bookType => typeof this.booksType[bookType] === 'number'
+ bookTypeArr = Object.keys(BookType).filter(
+ (bookType) => typeof this.booksType[bookType] === 'number'
);
loading = false;
@@ -917,7 +834,7 @@ export class BookListComponent implements OnInit {
form: FormGroup;
- constructor(private store: Store, private fb: FormBuilder) { }
+ constructor(private store: Store, private fb: FormBuilder) {}
ngOnInit() {
this.get();
@@ -925,9 +842,10 @@ export class BookListComponent implements OnInit {
get() {
this.loading = true;
- this.store.dispatch(new GetBooks()).subscribe(() => {
- this.loading = false;
- });
+ this.store
+ .dispatch(new GetBooks())
+ .pipe(finalize(() => (this.loading = false)))
+ .subscribe(() => {});
}
createBook() {
@@ -965,35 +883,16 @@ 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:
+Open `book-list.component.ts` file in `app\book\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 { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
-import { Books } from '../../store/models';
-import { GetBooks, CreateUpdateBook } from '../../store/actions'; //<== added CreateUpdateBook ==>
+import { finalize } from 'rxjs/operators';
+import { BookDto, BookType } from '../models';
+import { GetBooks, CreateUpdateBook } from '../state/book.actions'; // <== added CreateUpdateBook ==>
+import { BookState } from '../state/book.state';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
@@ -1001,16 +900,16 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
- providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }]
+ providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookListComponent implements OnInit {
- @Select(BooksState.getBooks)
- books$: Observable;
+ @Select(BookState.getBooks)
+ books$: Observable;
- booksType = Books.BookType;
+ booksType = BookType;
- bookTypeArr = Object.keys(Books.BookType).filter(
- bookType => typeof this.booksType[bookType] === 'number'
+ bookTypeArr = Object.keys(BookType).filter(
+ (bookType) => typeof this.booksType[bookType] === 'number'
);
loading = false;
@@ -1019,7 +918,7 @@ export class BookListComponent implements OnInit {
form: FormGroup;
- constructor(private store: Store, private fb: FormBuilder) { }
+ constructor(private store: Store, private fb: FormBuilder) {}
ngOnInit() {
this.get();
@@ -1027,9 +926,10 @@ export class BookListComponent implements OnInit {
get() {
this.loading = true;
- this.store.dispatch(new GetBooks()).subscribe(() => {
- this.loading = false;
- });
+ this.store
+ .dispatch(new GetBooks())
+ .pipe(finalize(() => (this.loading = false)))
+ .subscribe(() => {});
}
createBook() {
@@ -1046,7 +946,7 @@ export class BookListComponent implements OnInit {
});
}
- //<== added save ==>
+ // <== added save ==>
save() {
if (this.form.invalid) {
return;
@@ -1062,92 +962,102 @@ export class BookListComponent implements OnInit {
```
* We imported `CreateUpdateBook`.
-* We added `save` method
-
-### Updating an existing book
+* We added `save` method
-#### BooksService
+Open `book-list.component.html` in `app\book\book-list` folder and add the following `abp-button` to save the new book.
-Open the `books.service.ts` in `app\books\shared` folder and add the `getById` and `update` methods.
+```html
+
+
+
+
+
+
+```
-```js
-getById(id: string): Observable {
- return this.restService.request({
- method: 'GET',
- url: `/api/app/book/${id}`
- });
-}
+Find the `