diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 10c51a9ca5a..ac222d99dec 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -50,7 +50,7 @@ "path": "tutorials" }, { - "text": "TODO application", + "text": "TODO Application", "items": [ { "text": "Overview", @@ -67,7 +67,7 @@ ] }, { - "text": "Book Store application", + "text": "Book Store Application", "items": [ { "text": "Overview", @@ -115,6 +115,39 @@ } ] }, + { + "text": "Modular Monolith Application", + "items": [ + { + "text": "Overview", + "path": "tutorials/modular-crm/index.md" + }, + { + "text": "1: Creating the initial solution", + "path": "tutorials/modular-crm/part-01.md" + }, + { + "text": "2: Creating the initial Products module", + "path": "tutorials/modular-crm/part-02.md" + }, + { + "text": "3: Building the Products module", + "path": "tutorials/modular-crm/part-03.md" + }, + { + "text": "4: Creating the initial Ordering module", + "path": "tutorials/modular-crm/part-04.md" + }, + { + "text": "5: Building the Ordering module", + "path": "tutorials/modular-crm/part-05.md" + }, + { + "text": "6: Integrating the modules", + "path": "tutorials/modular-crm/part-06.md" + } + ] + }, { "text": "Community Articles", "path": "https://abp.io/community" diff --git a/docs/en/framework/architecture/best-practices/index.md b/docs/en/framework/architecture/best-practices/index.md index 49b1e5ec0f7..1dc84838c6a 100644 --- a/docs/en/framework/architecture/best-practices/index.md +++ b/docs/en/framework/architecture/best-practices/index.md @@ -1,7 +1,5 @@ # Module Development Best Practices & Conventions -### Introduction - This document describes the **best practices** and **conventions** for those who want to develop **modules** that satisfy the following specifications: * Develop modules that conform to the **Domain Driven Design** patterns & best practices. @@ -10,7 +8,7 @@ This document describes the **best practices** and **conventions** for those who Also, this guide is mostly usable for general **application development**. -### Guides +## The Guides * Overall * [Module Architecture](./module-architecture.md) diff --git a/docs/en/tutorials/index.md b/docs/en/tutorials/index.md index aac3abfaed3..87484d2acbe 100644 --- a/docs/en/tutorials/index.md +++ b/docs/en/tutorials/index.md @@ -3,4 +3,5 @@ The following guides explains how to build ABP based applications: * [TODO Application](todo/index.md): This is a single-part, quick-start tutorial to build a simple application with ABP. Start with this tutorial if you want to quickly understand how ABP works. -* [Book Store Application](book-store/overview.md): This is a multi-part, complete tutorial to build a bookstore application with ABP. Start with this tutorial if you want to create a layered solution with ABP and apply DDD best practices. \ No newline at end of file +* [Book Store Application](book-store/index.md): This is a multi-part, complete tutorial to build a bookstore application with ABP. Start with this tutorial if you want to create a layered solution with ABP and apply DDD best practices. +* [Modular Monolith Application](modular-crm/index.md): This is a multi-part tutorial that demonstrates how to create application modules, compose and communicate them to build a monolith modular web application. \ No newline at end of file diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-entity-framework-core-migration-dialog.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-entity-framework-core-migration-dialog.png new file mode 100644 index 00000000000..c96217ac472 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-entity-framework-core-migration-dialog.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-entity-framework-core-migration.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-entity-framework-core-migration.png new file mode 100644 index 00000000000..1629f541d87 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-entity-framework-core-migration.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-new-dd-module.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-dd-module.png new file mode 100644 index 00000000000..f8a701770e1 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-dd-module.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-new-empty-module-dialog.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-empty-module-dialog.png new file mode 100644 index 00000000000..882323b4b6c Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-empty-module-dialog.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-new-empty-module.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-empty-module.png new file mode 100644 index 00000000000..8815d15c449 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-empty-module.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-new-folder-command.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-folder-command.png new file mode 100644 index 00000000000..6557599a4e5 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-folder-command.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-new-package-class-library.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-package-class-library.png new file mode 100644 index 00000000000..247ee96ca03 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-package-class-library.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-new-package-mvc-ui.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-package-mvc-ui.png new file mode 100644 index 00000000000..41214c0ddc1 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-package-mvc-ui.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-new-package.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-package.png new file mode 100644 index 00000000000..de9e1aa8c5e Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-new-package.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-nuget-package-reference.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-nuget-package-reference.png new file mode 100644 index 00000000000..c52df6d5599 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-nuget-package-reference.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-2.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-2.png new file mode 100644 index 00000000000..47a5671f7f4 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-2.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-3.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-3.png new file mode 100644 index 00000000000..e79b72747fe Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-3.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-4.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-4.png new file mode 100644 index 00000000000..fa3b5c4e238 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-4.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-5.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-5.png new file mode 100644 index 00000000000..4a8ef0ba580 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-5.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog-2.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog-2.png new file mode 100644 index 00000000000..09682f0b6fd Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog-2.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog-3.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog-3.png new file mode 100644 index 00000000000..2d9d1e504b8 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog-3.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog-4.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog-4.png new file mode 100644 index 00000000000..8480ee81515 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog-4.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog.png new file mode 100644 index 00000000000..34342dad2e7 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference.png new file mode 100644 index 00000000000..2cbd244a4b0 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-added-ddd-domain-package.png b/docs/en/tutorials/modular-crm/images/abp-studio-added-ddd-domain-package.png new file mode 100644 index 00000000000..ed03c6b3bb2 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-added-ddd-domain-package.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-browser-list-of-orders-with-product-name.png b/docs/en/tutorials/modular-crm/images/abp-studio-browser-list-of-orders-with-product-name.png new file mode 100644 index 00000000000..ecbd5a7fbcb Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-browser-list-of-orders-with-product-name.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-browser-list-of-products.png b/docs/en/tutorials/modular-crm/images/abp-studio-browser-list-of-products.png new file mode 100644 index 00000000000..f204d55ed66 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-browser-list-of-products.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-browser-orders-menu-item.png b/docs/en/tutorials/modular-crm/images/abp-studio-browser-orders-menu-item.png new file mode 100644 index 00000000000..cb3af02592f Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-browser-orders-menu-item.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-build-and-restart-application.png b/docs/en/tutorials/modular-crm/images/abp-studio-build-and-restart-application.png new file mode 100644 index 00000000000..010851ee05f Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-build-and-restart-application.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-create-new-module-dialog-step-db.png b/docs/en/tutorials/modular-crm/images/abp-studio-create-new-module-dialog-step-db.png new file mode 100644 index 00000000000..714eeac4832 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-create-new-module-dialog-step-db.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-create-new-module-dialog-step-ui.png b/docs/en/tutorials/modular-crm/images/abp-studio-create-new-module-dialog-step-ui.png new file mode 100644 index 00000000000..4f56ddf0706 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-create-new-module-dialog-step-ui.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-create-new-module-dialog.png b/docs/en/tutorials/modular-crm/images/abp-studio-create-new-module-dialog.png new file mode 100644 index 00000000000..681402ae449 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-create-new-module-dialog.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-entity-framework-core-add-migration-order.png b/docs/en/tutorials/modular-crm/images/abp-studio-entity-framework-core-add-migration-order.png new file mode 100644 index 00000000000..1e247f069b6 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-entity-framework-core-add-migration-order.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-entity-framework-core-update-database.png b/docs/en/tutorials/modular-crm/images/abp-studio-entity-framework-core-update-database.png new file mode 100644 index 00000000000..bece8dfc127 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-entity-framework-core-update-database.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-graph-build.png b/docs/en/tutorials/modular-crm/images/abp-studio-graph-build.png new file mode 100644 index 00000000000..11855d792ed Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-graph-build.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-import-module-dialog-for-ordering.png b/docs/en/tutorials/modular-crm/images/abp-studio-import-module-dialog-for-ordering.png new file mode 100644 index 00000000000..b39a3220a5f Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-import-module-dialog-for-ordering.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-import-module-dialog.png b/docs/en/tutorials/modular-crm/images/abp-studio-import-module-dialog.png new file mode 100644 index 00000000000..88acf77a3ce Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-import-module-dialog.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-import-module-for-ordering-dialog.png b/docs/en/tutorials/modular-crm/images/abp-studio-import-module-for-ordering-dialog.png new file mode 100644 index 00000000000..762265b2c31 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-import-module-for-ordering-dialog.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-import-module-for-ordering.png b/docs/en/tutorials/modular-crm/images/abp-studio-import-module-for-ordering.png new file mode 100644 index 00000000000..94ab70d2558 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-import-module-for-ordering.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-import-module-ordering.png b/docs/en/tutorials/modular-crm/images/abp-studio-import-module-ordering.png new file mode 100644 index 00000000000..e62c0dc315a Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-import-module-ordering.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-import-module.png b/docs/en/tutorials/modular-crm/images/abp-studio-import-module.png new file mode 100644 index 00000000000..673d3033070 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-import-module.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-imports-and-dependencies.png b/docs/en/tutorials/modular-crm/images/abp-studio-imports-and-dependencies.png new file mode 100644 index 00000000000..30d484ba6be Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-imports-and-dependencies.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog-for-ordering.png b/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog-for-ordering.png new file mode 100644 index 00000000000..21af63a949b Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog-for-ordering.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog.png b/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog.png new file mode 100644 index 00000000000..17eb6d0ecc9 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-modular-crm-with-two-modules.png b/docs/en/tutorials/modular-crm/images/abp-studio-modular-crm-with-two-modules.png new file mode 100644 index 00000000000..3e04717081a Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-modular-crm-with-two-modules.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-module-installation-dialog.png b/docs/en/tutorials/modular-crm/images/abp-studio-module-installation-dialog.png new file mode 100644 index 00000000000..9946afaaec9 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-module-installation-dialog.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-new-folder-dialog.png b/docs/en/tutorials/modular-crm/images/abp-studio-new-folder-dialog.png new file mode 100644 index 00000000000..f726332a370 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-new-folder-dialog.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-new-package-under-the-module.png b/docs/en/tutorials/modular-crm/images/abp-studio-new-package-under-the-module.png new file mode 100644 index 00000000000..8ac9bb987e5 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-new-package-under-the-module.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-open-in-explorer.png b/docs/en/tutorials/modular-crm/images/abp-studio-open-in-explorer.png new file mode 100644 index 00000000000..ce052d2b170 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-open-in-explorer.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-open-with-visual-studio-main-app.png b/docs/en/tutorials/modular-crm/images/abp-studio-open-with-visual-studio-main-app.png new file mode 100644 index 00000000000..1f879ed7952 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-open-with-visual-studio-main-app.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-open-with-visual-studio.png b/docs/en/tutorials/modular-crm/images/abp-studio-open-with-visual-studio.png new file mode 100644 index 00000000000..ba96d6f488c Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-open-with-visual-studio.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-project-reference-example.png b/docs/en/tutorials/modular-crm/images/abp-studio-project-reference-example.png new file mode 100644 index 00000000000..7694d8013bd Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-project-reference-example.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-run-initially.png b/docs/en/tutorials/modular-crm/images/abp-studio-run-initially.png new file mode 100644 index 00000000000..2f969eff5b5 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-run-initially.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-solution-explorer-two-modules.png b/docs/en/tutorials/modular-crm/images/abp-studio-solution-explorer-two-modules.png new file mode 100644 index 00000000000..a98909c58b6 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-solution-explorer-two-modules.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-solution-explorer-with-folders.png b/docs/en/tutorials/modular-crm/images/abp-studio-solution-explorer-with-folders.png new file mode 100644 index 00000000000..221d0029220 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-solution-explorer-with-folders.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-graph-build.png b/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-graph-build.png new file mode 100644 index 00000000000..ececabbf01a Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-graph-build.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-initial-product-page.png b/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-initial-product-page.png new file mode 100644 index 00000000000..480da27d7eb Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-initial-product-page.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-orders-page.png b/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-orders-page.png new file mode 100644 index 00000000000..f3e9896e8f3 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-orders-page.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-swagger-create-order.png b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-create-order.png new file mode 100644 index 00000000000..e2c15fca239 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-create-order.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-swagger-list-orders.png b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-list-orders.png new file mode 100644 index 00000000000..e58ea0c51dd Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-list-orders.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-swagger-ui-create-product-execute.png b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-ui-create-product-execute.png new file mode 100644 index 00000000000..e07804e37d2 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-ui-create-product-execute.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-swagger-ui-create-product-try.png b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-ui-create-product-try.png new file mode 100644 index 00000000000..52a99ce8fa2 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-ui-create-product-try.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-swagger-ui-in-browser.png b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-ui-in-browser.png new file mode 100644 index 00000000000..8183cde1fb3 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-ui-in-browser.png differ diff --git a/docs/en/tutorials/modular-crm/images/file-system-odering-module-initial-folder.png b/docs/en/tutorials/modular-crm/images/file-system-odering-module-initial-folder.png new file mode 100644 index 00000000000..eb10f3e26ee Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/file-system-odering-module-initial-folder.png differ diff --git a/docs/en/tutorials/modular-crm/images/product-module-folder.png b/docs/en/tutorials/modular-crm/images/product-module-folder.png new file mode 100644 index 00000000000..f9778d4b868 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/product-module-folder.png differ diff --git a/docs/en/tutorials/modular-crm/images/product-module-visual-studio.png b/docs/en/tutorials/modular-crm/images/product-module-visual-studio.png new file mode 100644 index 00000000000..843326afede Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/product-module-visual-studio.png differ diff --git a/docs/en/tutorials/modular-crm/images/products-orders-pages-crop.png b/docs/en/tutorials/modular-crm/images/products-orders-pages-crop.png new file mode 100644 index 00000000000..dbd8bbdee14 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/products-orders-pages-crop.png differ diff --git a/docs/en/tutorials/modular-crm/images/solution-explorer-modular-crm-expanded.png b/docs/en/tutorials/modular-crm/images/solution-explorer-modular-crm-expanded.png new file mode 100644 index 00000000000..841b8fe610e Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/solution-explorer-modular-crm-expanded.png differ diff --git a/docs/en/tutorials/modular-crm/images/solution-explorer-modular-crm-initial.png b/docs/en/tutorials/modular-crm/images/solution-explorer-modular-crm-initial.png new file mode 100644 index 00000000000..d2240688ec8 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/solution-explorer-modular-crm-initial.png differ diff --git a/docs/en/tutorials/modular-crm/images/sql-server-orders-database-table.png b/docs/en/tutorials/modular-crm/images/sql-server-orders-database-table.png new file mode 100644 index 00000000000..2090a35673e Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/sql-server-orders-database-table.png differ diff --git a/docs/en/tutorials/modular-crm/images/sql-server-orders-table-content.png b/docs/en/tutorials/modular-crm/images/sql-server-orders-table-content.png new file mode 100644 index 00000000000..1bc432c650d Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/sql-server-orders-table-content.png differ diff --git a/docs/en/tutorials/modular-crm/images/sql-server-products-database-table-filled.png b/docs/en/tutorials/modular-crm/images/sql-server-products-database-table-filled.png new file mode 100644 index 00000000000..051f2044f88 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/sql-server-products-database-table-filled.png differ diff --git a/docs/en/tutorials/modular-crm/images/sql-server-products-database-table.png b/docs/en/tutorials/modular-crm/images/sql-server-products-database-table.png new file mode 100644 index 00000000000..803c0085249 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/sql-server-products-database-table.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-application-contracts.png b/docs/en/tutorials/modular-crm/images/visual-studio-application-contracts.png new file mode 100644 index 00000000000..9dfdcc0f342 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-application-contracts.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-main-dbcontext.png b/docs/en/tutorials/modular-crm/images/visual-studio-main-dbcontext.png new file mode 100644 index 00000000000..825b6c24229 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-main-dbcontext.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-modular-crm-solution.png b/docs/en/tutorials/modular-crm/images/visual-studio-modular-crm-solution.png new file mode 100644 index 00000000000..ef96d4b9325 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-modular-crm-solution.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-new-migration-class-2.png b/docs/en/tutorials/modular-crm/images/visual-studio-new-migration-class-2.png new file mode 100644 index 00000000000..3acc6e45c7f Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-new-migration-class-2.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-new-migration-class.png b/docs/en/tutorials/modular-crm/images/visual-studio-new-migration-class.png new file mode 100644 index 00000000000..b18d071cb13 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-new-migration-class.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-order-entity.png b/docs/en/tutorials/modular-crm/images/visual-studio-order-entity.png new file mode 100644 index 00000000000..33ea2b9d24b Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-order-entity.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-order-event-handler.png b/docs/en/tutorials/modular-crm/images/visual-studio-order-event-handler.png new file mode 100644 index 00000000000..cae742abb3c Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-order-event-handler.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-order-event.png b/docs/en/tutorials/modular-crm/images/visual-studio-order-event.png new file mode 100644 index 00000000000..21377ac3ee2 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-order-event.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-order-reporting-app-service-impl.png b/docs/en/tutorials/modular-crm/images/visual-studio-order-reporting-app-service-impl.png new file mode 100644 index 00000000000..50074867752 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-order-reporting-app-service-impl.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-order-reporting-app-service.png b/docs/en/tutorials/modular-crm/images/visual-studio-order-reporting-app-service.png new file mode 100644 index 00000000000..7333958f11e Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-order-reporting-app-service.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-ordering-controller.png b/docs/en/tutorials/modular-crm/images/visual-studio-ordering-controller.png new file mode 100644 index 00000000000..56a1dad5b11 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-ordering-controller.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-ordering-module-initial.png b/docs/en/tutorials/modular-crm/images/visual-studio-ordering-module-initial.png new file mode 100644 index 00000000000..0b238370b1f Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-ordering-module-initial.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-ordering-ui-package-dependency.png b/docs/en/tutorials/modular-crm/images/visual-studio-ordering-ui-package-dependency.png new file mode 100644 index 00000000000..4fe2d6f935b Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-ordering-ui-package-dependency.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-pages-folder.png b/docs/en/tutorials/modular-crm/images/visual-studio-pages-folder.png new file mode 100644 index 00000000000..f1b5742ce9f Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-pages-folder.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-product-integration-service-implementation.png b/docs/en/tutorials/modular-crm/images/visual-studio-product-integration-service-implementation.png new file mode 100644 index 00000000000..48ea4cec666 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-product-integration-service-implementation.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-product-integration-service.png b/docs/en/tutorials/modular-crm/images/visual-studio-product-integration-service.png new file mode 100644 index 00000000000..a211c2357e3 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-product-integration-service.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-products-cshtml.png b/docs/en/tutorials/modular-crm/images/visual-studio-products-cshtml.png new file mode 100644 index 00000000000..24f4ae46770 Binary files /dev/null and b/docs/en/tutorials/modular-crm/images/visual-studio-products-cshtml.png differ diff --git a/docs/en/tutorials/modular-crm/index.md b/docs/en/tutorials/modular-crm/index.md new file mode 100644 index 00000000000..2fb7f18b981 --- /dev/null +++ b/docs/en/tutorials/modular-crm/index.md @@ -0,0 +1,34 @@ +# Modular Monolith Application Development Tutorial + +````json +//[doc-nav] +{ + "Next": { + "Name": "Creating the initial solution", + "Path": "tutorials/modular-crm/part-01" + } +} +```` + +ABP provides a great infrastructure and tooling to build modular software solutions. In this tutorial, you will learn how to create application modules, compose and communicate them to build a monolith modular web application. + +> **This tutorial focuses on modularity.** The example application's functionality and user interface are intentionally kept simple. If you want to learn real world, full featured application logic development with ABP, please follow the [Book Store tutorial](../book-store/index.md). + +## Tutorial Outline + +This tutorial is organized as the following parts: + +* [Part 01: Creating the initial solution](part-01.md) +* [Part 02: Creating the initial Products module](part-02.md) +* [Part 03: Building the Products module](part-03.md) +* [Part 04: Creating the initial Ordering module](part-04.md) +* [Part 05: Building the Ordering module](part-05.md) +* [Part 06: Integrating the modules](part-06.md) + +## Download the Source Code + +You can download the completed sample solution [here](https://github.com/abpframework/abp-samples/tree/master/ModularCRM). + +## See Also + +* [Book Store: Web Application Development Tutorial](../book-store/index.md) \ No newline at end of file diff --git a/docs/en/tutorials/modular-crm/part-01.md b/docs/en/tutorials/modular-crm/part-01.md new file mode 100644 index 00000000000..98698b481df --- /dev/null +++ b/docs/en/tutorials/modular-crm/part-01.md @@ -0,0 +1,37 @@ +# Creating the Initial Solution + +````json +//[doc-nav] +{ + "Next": { + "Name": "Creating the initial Products module", + "Path": "tutorials/modular-crm/part-02" + } +} +```` + +Follow the [Get Stared](../../get-started/layered-web-application.md) guide to create a new layered web application with the following configuration: + +* **Solution name**: `ModularCrm` +* **UI Framework**: ASP.NET Core MVC / Razor Pages +* **Database Provider**: Entity Framework Core + +You can select the other options based on your preference. + +> **Please complete the [Get Stared](../../get-started/layered-web-application.md) guide and run the web application before going further.** + +The initial solution structure should be like the following in ABP Studio's *Solution Explorer*: + +![solution-explorer-modular-crm-initial](images/solution-explorer-modular-crm-initial.png) + +Initially, you see a `ModularCrm` solution and a `ModularCrm` module under that solution. + +> An ABP Studio module is typically a .NET solution and an ABP Studio solution is an umbrella concept for multiple .NET Solutions (see the [concepts](../../studio/concepts.md) document for more). + +`ModularCrm` module is your main application, which is a layered .NET solution that consists of several packages (.NET projects). You can expand the `ModularCrm` module to see its packages: + +![solution-explorer-modular-crm-expanded](images/solution-explorer-modular-crm-expanded.png) + +## Summary + +We've created the initial layered monolith solution. In the next part, we will learn how to create a new application module and install it to the main application. \ No newline at end of file diff --git a/docs/en/tutorials/modular-crm/part-02.md b/docs/en/tutorials/modular-crm/part-02.md new file mode 100644 index 00000000000..edc6f6bb384 --- /dev/null +++ b/docs/en/tutorials/modular-crm/part-02.md @@ -0,0 +1,138 @@ +# Creating the Initial Products Module + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Creating the initial solution", + "Path": "tutorials/modular-crm/part-01" + }, + "Next": { + "Name": "Building the Products module", + "Path": "tutorials/modular-crm/part-03" + } +} +```` + +In this part, you will build a new module for product management and install it to the main CRM application. + +## Creating Solution Folders + +You can create solution folders and sub-folders in *Solution Explorer* to better organize your solution components. Right-click to the solution root on the *Solution Explorer* panel, and select *Add* -> *New Folder* command: + +![abp-studio-add-new-folder-command](images/abp-studio-add-new-folder-command.png) + +That command opens a dialog where you can set the folder name: + +![abp-studio-new-folder-dialog](images/abp-studio-new-folder-dialog.png) + +Create `main` and `modules` folder using the *New Folder* command, then move the `ModularCrm` module into the `main` folder (simply by drag & drop). The *Solution Explorer* panel should look like the following figure now: + +![abp-studio-solution-explorer-with-folders](images/abp-studio-solution-explorer-with-folders.png) + +## Creating The Module + +There are two module templates provided by ABP Studio: + +* **Empty Module**: You can use that module template to build your module structure from scratch. +* **DDD Module**: A Domain-Driven Design based layered module structure. + +We will use the *DDD Module* template for the Product module. We will use the *Empty Module* template later in this tutorial. + +Right-click the `modules` folder on the *Solution Explorer* panel, and select the *Add* -> *New Module* -> *DDD Module* command: + +![abp-studio-add-new-dd-module](images/abp-studio-add-new-dd-module.png) + +This command opens a new dialog to define properties of the new module. You can use the following values to create a new module named `ModularCrm.Products`: + +![abp-studio-create-new-module-dialog](images/abp-studio-create-new-module-dialog.png) + +When you click the *Next* button, you are redirected to the UI selection step. + +### Selecting the UI Type + +Here, you can select the UI type you want to support in your module: + +![abp-studio-create-new-module-dialog-step-ui](images/abp-studio-create-new-module-dialog-step-ui.png) + +A module; + +* May not contain a UI and leaves the UI development to the final application. +* May contain a single UI implementation that is typically in the same technology with the main application. +* May contain more than one UI implementation if you want to create a reusable application module and you want to make that module usable by different applications with different UI technologies. For example, all of [pre-built ABP modules](https://abp.io/modules) support multiple UI options. + +In this tutorial, we are selecting the MVC UI since we are building that module only for our `ModularCrm` solution and we are using the MVC UI in our application. So, select the MVC UI and click the *Next* button. + +### Selecting the Database Provider + +The next step is to select the database provider (or providers) you want to support with your module: + +![abp-studio-create-new-module-dialog-step-db](images/abp-studio-create-new-module-dialog-step-db.png) + +Since our main application is using Entity Framework Core and we will use the `ModularCrm.Products` module only for that main application, we can select the *Entity Framework Core* option and click the *Create* button. + +### Exploring the New Module + +After adding the new module, the *Solution Explorer* panel should look like the following figure: + +![abp-studio-solution-explorer-two-modules](images/abp-studio-solution-explorer-two-modules.png) + +The new `ModularCrm.Products` module has been created and added to the solution. The `ModularCrm.Products` module has a separate and independent .NET solution. Right-click the `ModularCrm.Products` module and select the *Open with* -> *Explorer* command: + +![abp-studio-open-in-explorer](images/abp-studio-open-in-explorer.png) + +This command opens the solution folder in your file system: + +![product-module-folder](images/product-module-folder.png) + +You can open `ModularCrm.Products.sln` in your favorite IDE (e.g. Visual Studio): + +![product-module-visual-studio](images/product-module-visual-studio.png) + +As seen in the preceding figure, the `ModularCrm.Products` solution consists of several layers, each has own responsibility. + +### Installing the Product Module to the Main Application + +A module does not contain an executable application inside. The `Modular.Products.Web` project is just a class library project, not an executable web application. A module should be installed to an executable application in order to run it. + +> **Ensure that the web application is not running in [Solution Runner](../../studio/running-applications.md) or in your IDE. Installing a module to a running application will produce errors.** + +The product module has no relation to the main application yet. Right-click to the `ModularCrm` module (inside the `main` folder) and select the *Import Module* command: + +![abp-studio-import-module](images/abp-studio-import-module.png) + +The *Import Module* command opens a dialog as shown below: + +![abp-studio-import-module-dialog](images/abp-studio-import-module-dialog.png) + +Select the `ModularCrm.Products` module and check the *Install this module* option. If you don't check that option, it only imports the module but doesn't setup project dependencies. Importing a module without installation can be used to manually setup your project dependencies. Here, we want to make it automatically, so checking the *Install this module* option. + +When you click the *OK* button, ABP Studio opens the *Install Module* dialog: + +![abp-studio-module-installation-dialog](images/abp-studio-module-installation-dialog.png) + +This dialog simplifies installing a multi-layer module to a multi-layer application. It automatically determines which package of the `ModularCrm.Products` module should be installed to which package of the main application. For example, the `ModularCrm.Products.Domain` package is installed to the `ModularCrm.Domain` package. In that way, you can use domain objects ([entities](../../framework/architecture/domain-driven-design/entities.md), [repositories](../../framework/architecture/domain-driven-design/repositories.md), ...) of the products module from the domain layer of your main application. + +The default package match is good for this tutorial, so you can click the *OK* button to proceed. + +### Building the Main Application + +After the installation, build the entire solution by right-clicking to the `ModularCrm` module (under the `main` folder) and select the *Dotnet CLI* -> *Graph Build* command: + +![abp-studio-graph-build](images/abp-studio-graph-build.png) + +Graph Build is a dotnet CLI command that recursively build all the referenced dotnet projects even if they are not a part of the root solution. + +> While developing multi-module solutions with ABP Studio, you may need to perform *Graph Build* on the root/main module if you made changes in the depending modules. + +### Run the Main Application + +Open the *Solution Runner* panel, click the *Play* button (near to the solution root), right-click the `ModularCrm.Web` application and select the *Browse* command. It will open the web application in the built-in browser. Then you can navigate to the *Products* page on the main menu of the application to see the Products page that is coming from the `ModularCrm.Products` module: + +![abp-studio-solution-runner-initial-product-page](images/abp-studio-solution-runner-initial-product-page.png) + +## Summary + +In this part, we've created a new module to manage products in our modular application. Then we installed the new module to the main application and run the solution to test if it has successfully installed. + +In the next part, you will learn how to create entities, services and a basic user interface for the products module. diff --git a/docs/en/tutorials/modular-crm/part-03.md b/docs/en/tutorials/modular-crm/part-03.md new file mode 100644 index 00000000000..c3230e8c025 --- /dev/null +++ b/docs/en/tutorials/modular-crm/part-03.md @@ -0,0 +1,506 @@ +# Building the Products Module + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Creating the initial Products module", + "Path": "tutorials/modular-crm/part-02" + }, + "Next": { + "Name": "Creating the initial Ordering module", + "Path": "tutorials/modular-crm/part-04" + } +} +```` + +In this part, you will learn how to create entities, services and a basic user interface for the products module. + +> **This module's functionality will be minimal to focus on modularity.** You can follow the [Book Store tutorial](../book-store/index.md) to learn building more real-world applications with ABP. + +If it is still running, please stop the web application before continuing with the tutorial. + +## Creating a `Product` Entity + +Open the `ModularCrm.Products` module in your favorite IDE. You can right-click the `ModularCrm.Products` module and select the *Open With* -> *Visual Studio* command to open the `ModularCrm.Products` module's .NET solution with Visual Studio. If you can not find your IDE in the *Open with* list, open with the *Explorer*, then open the `.sln` file with your IDE: + +![abp-studio-open-with-visual-studio](images/abp-studio-open-with-visual-studio.png) + +The `ModularCrm.Products` .NET solution should look like the following figure: + +![product-module-visual-studio](images/product-module-visual-studio.png) + +Add a new `Product` class under the `ModularCrm.Products.Domain` project (Right-click the `ModularCrm.Products.Domain` project, select *Add* -> *Class*): + +````csharp +using System; +using Volo.Abp.Domain.Entities; + +namespace ModularCrm.Products; + +public class Product : AggregateRoot +{ + public string Name { get; set; } + public int StockCount { get; set; } +} +```` + +## Mapping Entity to Database + +The next step is to configure Entity Framework Core `DbContext` class and the database for the new entity. + +### Add a `DbSet` Property + +Open the `ProductsDbContext` in the `ModularCrm.Products.EntityFrameworkCore` project and add a new `DbSet` property for the `Product` entity. The final `ProductsDbContext.cs` file content should be the following: + +````csharp +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; + +namespace ModularCrm.Products.EntityFrameworkCore; + +[ConnectionStringName(ProductsDbProperties.ConnectionStringName)] +public class ProductsDbContext : AbpDbContext, IProductsDbContext +{ + public DbSet Products { get; set; } //NEW: DBSET FOR THE PRODUCT ENTITY + + public ProductsDbContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + builder.ConfigureProducts(); + } +} +```` + +The `ProductsDbContext` class implements the `IProductsDbContext` interface. Add the following property to the `IProductsDbContext` interface: + +````csharp +DbSet Products { get; set; } +```` + +The final `IProductsDbContext` interface should be the following: + +````csharp +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; + +namespace ModularCrm.Products.EntityFrameworkCore; + +[ConnectionStringName(ProductsDbProperties.ConnectionStringName)] +public interface IProductsDbContext : IEfCoreDbContext +{ + DbSet Products { get; set; } +} +```` + +Having such an `IProductsDbContext` interface allows us to decouple our repositories (and other classes) from the concrete `ProductsDbContext` class, which provides flexibility to the final application to merge multiple `DbContext`s into a single `DbContext` to manage database migrations easier and have a database level transaction support for multi-module database operations. + +### Configure the Table Mapping + +The DDD module template has designed flexible so that your module can have a separate physical database, or store its tables inside another database, like the main database of your application. To make that possible, it configures the database mapping in an extension method (`ConfigureProducts`) that is called inside the `OnModelCreating` method above. Find that extension method (in the `ProductsDbContextModelCreatingExtensions` class) and change its content as the following code block: + +````csharp +using Microsoft.EntityFrameworkCore; +using Volo.Abp; +using Volo.Abp.EntityFrameworkCore.Modeling; + +namespace ModularCrm.Products.EntityFrameworkCore; + +public static class ProductsDbContextModelCreatingExtensions +{ + public static void ConfigureProducts( + this ModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + builder.Entity(b => + { + //Configure table & schema name + b.ToTable(ProductsDbProperties.DbTablePrefix + "Products", + ProductsDbProperties.DbSchema); + + //Always call this method to setup base entity properties + b.ConfigureByConvention(); + + //Properties of the entity + b.Property(q => q.Name).IsRequired().HasMaxLength(100); + }); + } +} +```` + +As first, we are setting the database table name with the `ToTable` method. `ProductsDbProperties.DbTablePrefix` defines a constant that is added as a prefix to all database table names of this module. If you see the `ProductsDbProperties` class (in the `ModularCrm.Products.Domain` project), `DbTablePrefix` value is `Products`. In that case, the table name for the `Product` entity will be `ProductsProducts`. We think that is unnecessary for such a simple module and we can remove that prefix. So, you can change the `ProductsDbProperties` class with the following content to set an empty string to the `DbTablePrefix` property: + +````csharp +namespace ModularCrm.Products; + +public static class ProductsDbProperties +{ + public static string DbTablePrefix { get; set; } = ""; + public static string? DbSchema { get; set; } = null; + public const string ConnectionStringName = "Products"; +} +```` + +You can set a `DbSchema` to collect a module's tables under a separate schema (if it is supported by your DBMS), or use a `DbTablePrefix` as a prefix for all tables names of a module. For this tutorial, we won't set any of them. + +In that point, build the `ModularCrm.Products` .NET solution in your IDE (or in ABP Studio UI). We will switch to the main application's .NET solution. + +### Configuring the Main Application Database + +We changed the Entity Framework Core configuration. The next step should be adding a new code-first database migration and update the database, so the new Products table is created on the database. + +We are not managing the database migrations in the module. Instead, the main application decides which DBMS (Database Management System) to use and how to share physical database(s) among modules. To keep this tutorial simple, we will store all the data of all modules in a single physical database. + +Open the `ModularCrm` module (which is the main application) in your IDE: + +![abp-studio-open-with-visual-studio-main-app](images/abp-studio-open-with-visual-studio-main-app.png) + +Find the `ModularCrmDbContext` class under the `ModularCrm.EntityFrameworkCore` project: + +![visual-studio-main-dbcontext](images/visual-studio-main-dbcontext.png) + +We will merge module's database configuration into `ModularCrmDbContext`. + +#### Replace the `IProductsDbContext` Service + +Follow the 3 steps below; + +**(1)** Add the following attribute on top of the `ModularCrmDbContext` class: + +````csharp +[ReplaceDbContext(typeof(IProductsDbContext))] +```` + +`ReplaceDbContext` attribute makes it possible to use the `ModularCrmDbContext` class in the services in the Products module. + +**(2)** Implement the `IProductsDbContext` by the `ModularCrmDbContext` class: + +````csharp +public class ModularCrmDbContext : + AbpDbContext, + ITenantManagementDbContext, + IIdentityDbContext, + IProductsDbContext //NEW: IMPLEMENT THE INTERFACE +{ + public DbSet Products { get; set; } //NEW: ADD DBSET PROPERTY + ... +} +```` + +**(3)** Finally, call the `ConfigureProducts()` extension method inside the `OnModelCreating` method after other `Configure...` module calls: + +````csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + ... + builder.ConfigureProducts(); //NEW: CALL THE EXTENSION METHOD +} +```` + +In this way, `ModularCrmDbContext` can be used by the products module over the `IProductsDbContext` interface. This part is only needed for one time for a module. Next time, you can just add a new database migration as explained in the next section. + +#### Add a Database Migration + +Now, we can add a new database migration. You can use Entity Framework Core's `Add-Migration` (or `dotnet ef migrations add`) terminal command, but we will use ABP Studio's shortcut UI in this tutorial. + +Ensure that the solution has built. You can right-click the `ModularCrm` (under the `main` folder) on ABP Studio *Solution Runner* and select the *Dotnet CLI* -> *Graph Build* command. + +Right-click the `ModularCrm.EntityFrameworkCore` package and select the *EF Core CLI* -> *Add Migration* command: + +![abp-studio-add-entity-framework-core-migration](images/abp-studio-add-entity-framework-core-migration.png) + +The *Add Migration* command opens a new dialog to get a migration name: + +![abp-studio-add-entity-framework-core-migration-dialog](images/abp-studio-add-entity-framework-core-migration-dialog.png) + +Once you click the *OK* button, a new database migration class is added into the `Migrations` folder of the `ModularCrm.EntityFrameworkCore` project: + +![visual-studio-new-migration-class](images/visual-studio-new-migration-class.png) + +Now, you can return to ABP Studio, right-click the `ModularCrm.EntityFrameworkCore` project and select the *EF Core CLI* -> *Update Database* command: + +![abp-studio-entity-framework-core-update-database](images/abp-studio-entity-framework-core-update-database.png) + +After the operation completes, you can check your database to see the new `Products` table has been created: + +![sql-server-products-database-table](images/sql-server-products-database-table.png) + +## Creating the Application Service + +Now, we will create an [application service](../../framework/architecture/domain-driven-design/application-services.md) to perform some use cases related to products. + +### Defining the Application Service Contract + +Return to your IDE (e.g. Visual Studio), open the `ModularCrm.Products` module's .NET solution and create an `IProductAppService` interface under the `ModularCrm.Products.Application.Contracts` project: + +````csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace ModularCrm.Products; + +public interface IProductAppService : IApplicationService +{ + Task> GetListAsync(); + Task CreateAsync(ProductCreationDto input); +} +```` + +We are defining application service interfaces and [data transfer objects](../../framework/architecture/domain-driven-design/data-transfer-objects.md) in the `Application.Contracts` project. In that way, we can share that contracts with clients without sharing the actual implementation class. + +### Defining Data Transfer Objects + +`GetListAsync` and `CreateAsync` methods are using `ProductDto` and `ProductCreationDto` classes that are not defined yet. So, we need to define them. + +Create a `ProductCreationDto` class under the `ModularCrm.Products.Application.Contracts` project: + +````csharp +using System.ComponentModel.DataAnnotations; + +namespace ModularCrm.Products; + +public class ProductCreationDto +{ + [Required] + [StringLength(100)] + public string Name { get; set; } + + [Range(0, int.MaxValue)] + public int StockCount { get; set; } +} +```` + +And create a `ProductDto` class under the `ModularCrm.Products.Application.Contracts` project: + +````csharp +using System; + +namespace ModularCrm.Products +{ + public class ProductDto + { + public Guid Id { get; set; } + public string Name { get; set; } + public int StockCount { get; set; } + } +} +```` + +The new files under the `ModularCrm.Products.Application.Contracts` project are shown below: + +![visual-studio-application-contracts](images/visual-studio-application-contracts.png) + +### Implementing the Application Service + +Now, we can implement the `IProductAppService` interface. Create a `ProductAppService` class under the `ModularCrm.Products.Application` project: + +````csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace ModularCrm.Products; + +public class ProductAppService : ProductsAppService, IProductAppService +{ + private readonly IRepository _productRepository; + + public ProductAppService(IRepository productRepository) + { + _productRepository = productRepository; + } + + public async Task> GetListAsync() + { + var products = await _productRepository.GetListAsync(); + return ObjectMapper.Map, List>(products); + } + + public async Task CreateAsync(ProductCreationDto input) + { + var product = new Product + { + Name = input.Name, + StockCount = input.StockCount + }; + + await _productRepository.InsertAsync(product); + } +} +```` + +Notice that `ProductAppService` class implements the `IProductAppService` and also inherits from the `ProductsAppService` class. Do not confuse about the naming (`ProductAppService` and `ProductsAppService`). The `ProductsAppService` is a base class. It makes a few configuration for [localization](../../framework/fundamentals/localization.md) and [object mapping](../../framework/infrastructure/object-to-object-mapping.md) (you can see in the `ModularCrm.Products.Application` project). You can inherit all of your application services from that base class. In this way, you can define some common properties and methods to share among all your application services. You can rename the base class if you feel that you may confuse later. + +#### Object Mapping + +`ProductAppService.GetListAsync` method uses the `ObjectMapper` service to convert `Product` entities to `ProductDto` objects. The mapping should be configured. Open the `ProductsApplicationAutoMapperProfile` class in the `ModularCrm.Products.Application` project and change it as the following code block: + +````csharp +using AutoMapper; + +namespace ModularCrm.Products; + +public class ProductsApplicationAutoMapperProfile : Profile +{ + public ProductsApplicationAutoMapperProfile() + { + CreateMap(); + } +} +```` + +We've just added the `CreateMap();` line to define the mapping. + +### Exposing Application Services as HTTP API Controllers + +For this application, we don't need to create HTTP API endpoints for the products module actually. But, it is good to understand how to do it when you need. You have two options; + +* You can create a regular ASP.NET Core Controller class into the `ModularCrm.Products.HttpApi` project, inject `IProductAppService` and use it to create wrapper methods. We will do it later while we will create the Ordering module. +* Alternatively, you can just use the ABP's [Auto API Controllers](../../framework/api-development/auto-controllers.md) feature to automatically expose your application services as API controllers by conventions. We will do it here. + +Open the `ModularCrmWebModule` class in the main application's solution (the `ModularCrm` solution), find the `PreConfigureServices` method and add the following lines inside that method: + +````csharp +PreConfigure(mvcBuilder => +{ + mvcBuilder.AddApplicationPartIfNotExists(typeof(ProductsApplicationModule).Assembly); +}); +```` + +This will tell to ASP.NET Core to explore the given assembly to discover controllers. + +Then open the `ConfigureAutoApiControllers` method of the same class and add a second `ConventionalControllers.Create` call as shown in the following code block: + +````csharp +Configure(options => +{ + options.ConventionalControllers.Create(typeof(ModularCrmApplicationModule).Assembly); + + //ADD THE FOLLOWING LINE: + options.ConventionalControllers.Create(typeof(ProductsApplicationModule).Assembly); +}); +```` + +This will tell to ABP framework to create API controllers for the application services in the given assembly. + +> We made these configurations in the main application's solution since there is no project in the product module's solution that has reference to ASP.NET Core MVC packages and uses the application layer of the product module. If you add a reference of `ModularCrm.Products.Application` to `ModularCrm.Products.HttpApi`, then you can move these configurations to the `ModularCrm.Products.HttpApi` project. + +Now, the application services defined in the `ModularCrm.Products.Application` project will be exposed as API controllers automatically by ABP. In the next section, we will use these API controller to create some example products. + +### Creating Example Products + +In this section, we will create a few example products using the [Swagger UI](../../framework/api-development/swagger.md). In that way, we will have some sample products to show on the UI. + +Now, right-click the `ModularCrm` under the `main` folder in the Solution Explorer panel, select the *Dotnet CLI* -> *Graph Build* command. In this way, we can be sure that the product module and the main application has built and ready to run. + +After the build process completes, open the Solution Runner panel and click the *Play* button near to the solution root. Once the `ModularCrm.Web` application runs, we can right-click it and select the *Browse* command to open the user interface. + +Once you see the user interface of the web application, type `/swagger` to the end of the URL to open the Swagger UI. If you scroll down, you should see the `Products` API: + +![abp-studio-swagger-ui-in-browser](images/abp-studio-swagger-ui-in-browser.png) + +Expand the `/api/app/product` API and click the *Try it out* button as shown in the following figure: + +![abp-studio-swagger-ui-create-product-try](images/abp-studio-swagger-ui-create-product-try.png) + +Then create a few products by filling the *Request body* and clicking the *Execute* button: + +![abp-studio-swagger-ui-create-product-execute](images/abp-studio-swagger-ui-create-product-execute.png) + +If you check the database, you should see the entities created in the `Products` table: + +![sql-server-products-database-table-filled](images/sql-server-products-database-table-filled.png) + +We've some entities in the database, and we can show them on the user interface now. + +## Creating the User Interface + +In this section, we will create a very simple user interface to demonstrate how to build UI in the products module and make it working in the main application. + +As a first step, stop the application on ABP Studio's Solution Runner if it is currently running. + +Open the `ModularCrm.Products` .NET solution in your IDE, find the `Pages/Products/Index.cshtml` file under the `ModularCrm.Products.Web` project: + +![visual-studio-products-cshtml](images/visual-studio-products-cshtml.png) + +Replace the `IndexModel.cshtml.cs` file with the following content: + +````csharp +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ModularCrm.Products.Web.Pages.Products; + +public class IndexModel : ProductsPageModel +{ + public List Products { get; set; } + + private readonly IProductAppService _productAppService; + + public IndexModel(IProductAppService productAppService) + { + _productAppService = productAppService; + } + + public async Task OnGetAsync() + { + Products = await _productAppService.GetListAsync(); + } +} +```` + +Here, we are simply using the `IProductAppService` to get a list of all products and assigning the result to the `Products` property. We can use it in the `Index.cshtml` file to show a simple list of products on the UI: + +````xml +@page +@using Microsoft.Extensions.Localization +@using ModularCrm.Products.Localization +@using ModularCrm.Products.Web.Pages.Products +@model ModularCrm.Products.Web.Pages.Products.IndexModel +@inject IStringLocalizer L + +

Products

+ + + + + @foreach (var product in Model.Products) + { + + @product.Name (stock: @product.StockCount) + + } + + + +```` + +You can build the product module's .NET solution (`ModularCrm.Products`), then right-click the `ModularCrm.Web` application on ABP Studio's solution runner and select the *Build & Restart* command: + +![abp-studio-build-and-restart-application](images/abp-studio-build-and-restart-application.png) + +Now, you can browse the *Products* page to see the list of the products: + +![abp-studio-browser-list-of-products](images/abp-studio-browser-list-of-products.png) + +As you see, it is pretty straightforward to develop a UI page in a modular ABP application. We kept the UI very simple to focus on the modularity. If you want to learn how to build complex application UIs, please check the [Book Store Tutorial](../book-store/index.md). + +## Final Notes + +Some of the projects in the product module's .NET solution (`ModularCrm.Products`) are not necessary for most of the cases. They are available to support different scenarios. You can delete them from your module (and remove the dependencies on the main application) if you want: + +* `ModularCrm.Products.HttpApi`: That project is to define regular HTTP API controllers. If you will always use ABP's [Auto API Controllers](../../framework/api-development/auto-controllers.md) feature (like we did in this tutorial), you can delete that project. +* `ModularCrm.Products.HttpApi.Client`: That project is generally shared with 3rd-party applications, so they can easily consume your HTTP API endpoints. In a modular monolith application, you typically don't need it. +* `ModularCrm.Products.HttpApi.Installer`: That project is used to discover and install a multi-projects module (like the product module) when you deploy it to a package management system (like NuGet). If you will use the module with local project references (like we did here), you can delete that project. +* You can also delete the test projects (there are 4 of them in the solution) if you don't prefer to write unit/integration tests in the module's solution (Legal warning: it is recommended to write tests 😊) diff --git a/docs/en/tutorials/modular-crm/part-04.md b/docs/en/tutorials/modular-crm/part-04.md new file mode 100644 index 00000000000..a8938fd6188 --- /dev/null +++ b/docs/en/tutorials/modular-crm/part-04.md @@ -0,0 +1,112 @@ +# Creating the Initial Ordering Module + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Building the Products Module", + "Path": "tutorials/modular-crm/part-03" + }, + "Next": { + "Name": "Building the Ordering module", + "Path": "tutorials/modular-crm/part-05" + } +} +```` + +In this part, you will build a new module for placing orders and install it to the main CRM application. + +## Creating an Empty Module + +We have used the *DDD Module* template for the Product module. We will use the *Empty Module* template for the Ordering module in this part. + +Right-click the `modules` folder on the *Solution Explorer* panel, and select the *Add* -> *New Module* -> *Empty Module* command: + +![abp-studio-add-new-empty-module](images/abp-studio-add-new-empty-module.png) + +That command opens a dialog to define the properties of the new module: + +![abp-studio-add-new-empty-module-dialog](images/abp-studio-add-new-empty-module-dialog.png) + +Set `ModularCrm.Ordering` as the *Module name*, leave the *Output folder* as is and click the *Create* button. It will create the new `ModularCrm.Ordering` module under the `modules` folder in the *Solution Explorer*: + +![abp-studio-modular-crm-with-two-modules](images/abp-studio-modular-crm-with-two-modules.png) + +Since we've created an empty module, it is really empty. If you open the `modules/modularcrm.ordering` in your file system, you can see the initial files: + +![file-system-odering-module-initial-folder](images/file-system-odering-module-initial-folder.png) + +## Creating the Module Packages + +In this section, we will create packages under the Ordering module. The Products module was well layered based on Domain Driven Design principles. This time, we will keep the things very simple and will only create two packaged for the Ordering module: + +* `ModularCrm.Ordering`: Contains all the module code without any layering. It will contains entities, database access code, services, controllers, UI pages and whatever we need to implement the *Ordering* module. +* `ModularCrm.Ordering.Contracts`: Contains the services and objects we want to share with other modules. `ModularCrm.Ordering` uses (and implements) this package. + +> If your modules are relatively small, easy to maintain, they will only be used by your main application and you don't care about layering, you can create such simple module structures. + +We will create and configure everything from scratch. You have already learned the easy way in the previous parts where we've created the Products module. It is time to have a deep understanding of the details and learn how ABP Studio allows you to setup custom structures. Let's begin... + +### Creating the `ModularCrm.Ordering.Contracts` Package + +We will start by creating the `ModularCrm.Ordering.Contracts` package. Right-click the `ModularCrm.Ordering` module on the *Solution Explorer* and select the *Add* -> *Package* -> *New Package* command as shown in the following figure: + +![abp-studio-add-new-package](images/abp-studio-add-new-package.png) + +That command opens a new dialog to create a new package: + +![abp-studio-add-new-package-class-library](images/abp-studio-add-new-package-class-library.png) + +With that dialog, you can build your module or application layer by layer. There are templates for any type of package. However, here we will go with the most simple one: *ABP Class Library*. ABP Class Library is an empty C# class library with the [core ABP package](https://www.nuget.org/packages/Volo.Abp.Core) dependency and a [module class](../../framework/architecture/modularity/basics.md). + +Type `ModularCrm.Ordering.Contracts` as the *Package name* and click the *Create* button. It will add the package under the `ModularCrm.Ordering` module: + +![abp-studio-new-package-under-the-module](images/abp-studio-new-package-under-the-module.png) + +### Creating the `ModularCrm.Ordering` Package + +Right-click the `ModularCrm.Ordering` module on the *Solution Explorer* again and select the *Add* -> *Package* -> *New Package* command to create a second package: + +![abp-studio-add-new-package-mvc-ui](images/abp-studio-add-new-package-mvc-ui.png) + +This time, we are selecting the *MVC UI* template and set the *Package name* as `ModularCrm.Ordering`. Click the *Create* button to add the new package. + +### Add Package Reference + +After the package has been added, right-click the `ModularCrm.Ordering` package and select the *Add Package Reference* command: + +![abp-studio-add-package-reference](images/abp-studio-add-package-reference.png) + +That command opens a dialog to select the package: + +![abp-studio-add-package-reference-dialog](images/abp-studio-add-package-reference-dialog.png) + +In that dialog, you can add package reference from various sources. Here, we will add reference for the package in the current module. So, select the `ModularCrm.Ordering.Contracts` package in *This module* tab and click the *OK* button. You can see the package reference under the *Projects* dependencies on the *Solution Explorer* panel: + +![abp-studio-project-reference-example](images/abp-studio-project-reference-example.png) + +The initial module creation has been completed. Now, we have a new module with 2 packages. However, it has no relation to other modules and applications in the solution yet. + +## Installing into the Main Application + +In this section, we will install the `ModularCrm.Ordering` module to the main application, so it can be a part of the system. + +> Before the installation, ensure that the web application is not running. + +Right-click the `ModularCrm` module (under the `main` folder) and select the *Import Module* command: + +![abp-studio-import-module-for-ordering](images/abp-studio-import-module-for-ordering.png) + +That command opens the *Import Module* dialog: + +![abp-studio-import-module-for-ordering-dialog](images/abp-studio-import-module-for-ordering-dialog.png) + +Select the `ModularCrm.Ordering` module and check the *Install this module* option as shown in the preceding figure. When you click the OK button, a new dialog is shown to select the packages to install: + +![abp-studio-install-module-dialog](images/abp-studio-install-module-dialog.png) + +Select the `ModuleCrm.Ordering` package on the left area, `ModularCrm.Domain` package on the middle area as shown in the preceding figure and click the *OK* button. + +> Since the Ordering module is not layered, we didn't install its packages to the layers of our main application. We are installing it only to `ModularCrm.Domain`. In this way, we can use the Ordering module from any layer of our application since `ModularCrm.Domain` is one of the core packages of our application. If you build your modules as non-layered and you don't have much code in the main application's .NET solution, you can also consider to create a non-layered main application that composes these modules. + +In this part of the tutorial, we've created an empty module and added packages. In that way, you can create modules or applications with custom structure. In the next part, we will add functionality to the Ordering module. \ No newline at end of file diff --git a/docs/en/tutorials/modular-crm/part-05.md b/docs/en/tutorials/modular-crm/part-05.md new file mode 100644 index 00000000000..4f4cb68f5e9 --- /dev/null +++ b/docs/en/tutorials/modular-crm/part-05.md @@ -0,0 +1,402 @@ +# Building the Ordering Module + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Creating the Initial Ordering Module", + "Path": "tutorials/modular-crm/part-04" + }, + "Next": { + "Name": "Integrating Modules", + "Path": "tutorials/modular-crm/part-06" + } +} +```` + +In the previous part, we've created a custom Ordering module and installed it into the main application. However, the Ordering module has no functionality now. In this part, we will create an `Order` entity and add functionality to create and list the orders. + +## Creating an `Order` Entity + +Open the `ModularCrm.Ordering` .NET solution in your IDE. + +> Tip: You can open the folder of a module's .NET solution by right-clicking the related module in ABP Studio, selecting the *Open with* -> *Explorer* command. + +The following figure shows the `ModularCrm.Ordering` module in the *Solution Explorer* panel of Visual Studio: + +![visual-studio-ordering-module-initial](images/visual-studio-ordering-module-initial.png) + +### Adding `Volo.Abp.Ddd.Domain` Package Reference + +As you see in the preceding figure, the solution structure is very minimal. It also has a minimal ABP dependency. ABP Framework has [hundreds of NuGet packages](https://abp.io/packages), but the `ModularCrm.Ordering` project only has the [`Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared`](https://abp.io/package-detail/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared) package reference: + +![visual-studio-ordering-ui-package-dependency](images/visual-studio-ordering-ui-package-dependency.png) + +We will create an [entity class](../../framework/architecture/domain-driven-design/entities.md) and ABP defines base entity classes and other related infrastructure in the [`Volo.Abp.Ddd.Domain`](https://abp.io/package-detail/Volo.Abp.Ddd.Domain) package. So, we need to add a reference to that NuGet package. + +You can add that package and arrange the [module](../../framework/architecture/modularity/basics.md) class dependency manually. However, here we will use ABP Studio as a more practical way. + +Return to ABP Studio, right-click the `ModularCrm.Ordering` package in the *Solution Explorer* and select the *Add Package Reference* command: + +![abp-studio-add-package-reference-2](images/abp-studio-add-package-reference-2.png) + +That command opens a dialog to add a new package reference: + +![abp-studio-add-nuget-package-reference](images/abp-studio-add-nuget-package-reference.png) + +Select the *NuGet* tab, type `Volo.Abp.Ddd.Domain` as the *Package name* and write the version of the package you want to install. Please be sure that you are installing exactly the same version with the other ABP packages you are already using. In future versions, ABP Studio will provide an easier way to select the package and its version. + +Click the *OK* button and then you can check the *Packages* under the `ModularCrm.Ordering` module *Dependencies* to see the `Volo.Abp.Ddd.Domain` package is installed: + +![abp-studio-added-ddd-domain-package](images/abp-studio-added-ddd-domain-package.png) + +### Adding an `Order` Class + +Now, you can return your IDE and add an `Order` class to the `ModularCrm.Ordering` project (open an `Entities` folder and place the `Order.cs` into that folder): + +````csharp +using System; +using ModularCrm.Ordering.Contracts.Enums; +using Volo.Abp.Domain.Entities.Auditing; + +namespace ModularCrm.Ordering.Entities +{ + public class Order : CreationAuditedAggregateRoot + { + public Guid ProductId { get; set; } + public string CustomerName { get; set; } + public OrderState State { get; set; } + } +} +```` + +We are allowing to place only a single product within an order. In a real-world application, the `Order` entity would be much more complex. However, the complexity of the `Order` entity doesn't effect modularity, so we keep it simple to focus on modularity in this tutorial. We are inheriting from the [`CreationAuditedAggregateRoot` class](../../framework/architecture/domain-driven-design/entities.md) since I want to know when an order has been created and who has created it. + +### Adding an `OrderState` Enumeration + +We used an `OrderState` enumeration that is not defined yet. Open an `Enums` folder in the `ModularCrm.Ordering.Contracts` project and create an `OrderState.cs` file inside it: + +````csharp +namespace ModularCrm.Ordering.Contracts.Enums; + +public enum OrderState : byte +{ + Placed = 0, + Delivered = 1, + Canceled = 2 +} +```` + +Final structure of the Ordering module should be similar to the following figure in your IDE: + +![visual-studio-order-entity](images/visual-studio-order-entity.png) + +## Configuring the Database Mapping + +The `Order` entity has been created. Now, we need to configure the database mapping for that entity. We will first install the Entity Framework Core package, define the database table mapping, create a database migration and update the database. + +### Installing the Entity Framework Core Package + +> In this section, we will install the [`Volo.Abp.EntityFrameworkCore`](https://abp.io/package-detail/Volo.Abp.EntityFrameworkCore) package to the Ordering module. That package is DBMS-independent and leaves the DBMS selection to the final application. If you want, you can install DBMS-specific package instead. For example, you can install the [`Volo.Abp.EntityFrameworkCore.SqlServer`](https://abp.io/package-detail/Volo.Abp.EntityFrameworkCore.SqlServer) package if you are using SQL server and want to make SQL Server specific configuration for your module's database. +> You can search for other packages on the [abp.io/packages](https://abp.io/packages) page. + +Stop the web application if it is still running. Return to ABP Studio, right-click the `ModularCrm.Ordering` package on the *Solution Explorer* panel and select the *Add Package Reference* command: + +![abp-studio-add-package-reference-3](images/abp-studio-add-package-reference-3.png) + +Select the *NuGet* tab, type `Volo.Abp.EntityFrameworkCore` as the *Package name* and specify a *Version* that is compatible with the ABP version used by your solution: + +![abp-studio-add-package-reference-dialog-2](images/abp-studio-add-package-reference-dialog-2.png) + +Once you click the *OK* button, the NuGet package reference is added. + +### Defining the Database Mappings + +Entity Framework Core requires to define a `DbContext` class as the main object for the database mapping. We want to use the main application's `DbContext` object. In that way, we can control the database migrations in a single point, ensure database transactions on multi-module operations and establish relations between database tables of different modules. However, the Ordering module can not use the main application's `DbContext` object, because it doesn't depend on the main application and we don't want to establish such a dependency. + +As a solution, we will define a `DbContext` interface in the Ordering module which is then implemented by the main module's `DbContext`. + +Open your IDE, create a `Data` folder under the `ModularCrm.Ordering` project, and create an `IOrderingDbContext` interface under that folder: + +````csharp +using Microsoft.EntityFrameworkCore; +using ModularCrm.Ordering.Entities; +using Volo.Abp.EntityFrameworkCore; + +namespace ModularCrm.Ordering.Data +{ + public interface IOrderingDbContext : IEfCoreDbContext + { + DbSet Orders { get; set; } + } +} +```` + +Now, we can inject and use the `IOrderingDbContext` in the Ordering module. Actually, we will not directly use that interface most of the time. Instead, we will use ABP's [repositories](../../framework/architecture/domain-driven-design/repositories.md). + +It is best to configure the database table mapping for the `Order` entity in the Ordering module. We will create an extension method that will be called by the main application later. Create a class named `OrderingDbContextModelCreatingExtensions` in the same `Data` folder: + +````csharp +using Microsoft.EntityFrameworkCore; +using ModularCrm.Ordering.Entities; +using Volo.Abp; +using Volo.Abp.EntityFrameworkCore.Modeling; + +namespace ModularCrm.Ordering.Data +{ + public static class OrderingDbContextModelCreatingExtensions + { + public static void ConfigureOrdering(this ModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + builder.Entity(b => + { + //Configure table name + b.ToTable("Orders"); + + //Always call this method to setup base entity properties + b.ConfigureByConvention(); + + //Properties of the entity + b.Property(q => q.CustomerName).IsRequired().HasMaxLength(120); + }); + } + } +} +```` + +#### Configuring the Main Application + +Open the main application's solution in your IDE, find the `ModularCrmDbContext` class under the `ModularCrm.EntityFrameworkCore` project and follow the 3 steps below: + +**(1)** Add the following attribute on top of the `ModularCrmDbContext` class: + +````csharp +[ReplaceDbContext(typeof(IOrderingDbContext))] +```` + +`ReplaceDbContext` attribute makes it possible to use the `ModularCrmDbContext` class in the services in the Ordering module. + +**(2)** Implement the `IOrderingDbContext` by the `ModularCrmDbContext` class: + +````csharp +public class ModularCrmDbContext : + AbpDbContext, + ITenantManagementDbContext, + IIdentityDbContext, + IProductsDbContext, + IOrderingDbContext //NEW: IMPLEMENT THE INTERFACE +{ + public DbSet Products { get; set; } + public DbSet Orders { get; set; } //NEW: ADD DBSET PROPERTY + ... +} +```` + +**(3)** Finally, call the `ConfigureOrdering()` extension method inside the `OnModelCreating` method after other `Configure...` module calls: + +````csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + ... + builder.ConfigureOrdering(); //NEW: CALL THE EXTENSION METHOD +} +```` + +In this way, `ModularCrmDbContext` can be used by the Ordering module over the `IProductsDbContext` interface. This part is only needed for one time for a module. Next time, you can just add a new database migration as explained in the next section. + +#### Add a Database Migration + +Now, we can add a new database migration. You can use Entity Framework Core's `Add-Migration` (or `dotnet ef migrations add`) terminal command, but we will use ABP Studio's shortcut UI in this tutorial. + +Ensure that the solution has built. You can right-click the `ModularCrm` (under the `main` folder) on ABP Studio *Solution Runner* and select the *Dotnet CLI* -> *Graph Build* command. + +Right-click the `ModularCrm.EntityFrameworkCore` package and select the *EF Core CLI* -> *Add Migration* command: + +![abp-studio-add-entity-framework-core-migration](images/abp-studio-add-entity-framework-core-migration.png) + +The *Add Migration* command opens a new dialog to get a migration name: + +![abp-studio-entity-framework-core-add-migration-order](images/abp-studio-entity-framework-core-add-migration-order.png) + +Once you click the *OK* button, a new database migration class is added into the `Migrations` folder of the `ModularCrm.EntityFrameworkCore` project: + +![visual-studio-new-migration-class-2](images/visual-studio-new-migration-class-2.png) + +Now, you can return to ABP Studio, right-click the `ModularCrm.EntityFrameworkCore` project and select the *EF Core CLI* -> *Update Database* command: + +![abp-studio-entity-framework-core-update-database](images/abp-studio-entity-framework-core-update-database.png) + +After the operation completes, you can check your database to see the new `Orders` table has been created: + +![sql-server-products-database-table](images/sql-server-orders-database-table.png) + +## Creating the User Interface + +Since this is a non-layered module, we can directly use entities and repositories on the user interface. If you think that is not a good practice, then use the layered module template as we've already done for the *Products* module. But for the Ordering module, we will keep it very simple for this tutorial to show it is also possible. + +### Creating a `_ViewImports.cshtml` File + +Open the `ModularCrm.Ordering` .NET solution in your favorite IDE, locate the `ModularCrm.Ordering` project and create a `Pages` folder under that project. Then add a `_ViewImports.cshtml` file under that `Pages` folder with the following content: + +````csharp +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling +```` + +That file simply imports some tag helpers from ASP.NET Core and ABP. The `Pages` folder should be like the following figure: + +![visual-studio-pages-folder](images/visual-studio-pages-folder.png) + +### Creating the Orders Page + +Create an `Orders` folder under the `Pages` folder and add an `Index.cshtml` Razor Page inside that new folder. Then replace the `Index.cshtml.cs` content with the following code block: + +````csharp +using Microsoft.AspNetCore.Mvc.RazorPages; +using ModularCrm.Ordering.Entities; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace ModularCrm.Ordering.Pages.Orders +{ + public class IndexModel : PageModel + { + public List Orders { get; set; } + + private readonly IRepository _orderRepository; + + public IndexModel(IRepository orderRepository) + { + _orderRepository = orderRepository; + } + + public async Task OnGetAsync() + { + Orders = await _orderRepository.GetListAsync(); + } + } +} +```` + +Here, we are injecting a repository to query `Order` entities from database to show on the page. Open the `Index.cshtml` file and replace the content with the following code block: + +````html +@page +@model ModularCrm.Ordering.Pages.Orders.IndexModel + +

Orders

+ + + + + @foreach (var order in Model.Orders) + { + + Customer: @order.CustomerName
+ Product: @order.ProductId
+ State: @order.State +
+ } +
+
+
+```` + +This page simply shows a list of orders on the UI. We haven't created a UI to create new orders, and we will not do it to keep this tutorial simple. If you want to learn how to create advanced UIs with ABP, please follow the [Book Store tutorial](../book-store/index.md). + +### Creating Some Sample Data + +You can open the database and manually create a few order records to show on the UI: + +![sql-server-orders-table-content](images/sql-server-orders-table-content.png) + +You can get `ProductId` values from the `Products` table and [generate](https://www.guidgenerator.com/) some random GUIDs for other GUID fields. + +### Building the Application + +Now, we will run the application to see the result. First of all, stop the application if it is already running. Then open the *Solution Runner* panel, right-click the `ModularCrm.Web` application, select the *Build* -> *Graph Build* command: + +![abp-studio-solution-runner-graph-build](images/abp-studio-solution-runner-graph-build.png) + +We've performed a graph build since we've made a change on a module and only building the main application is not enough. *Graph Build* command also builds the depended modules if it is necessary. Alternatively, you could build the Ordering module first (on ABP Studio or on your IDE), then Build right-click the `ModularCrm.Web` application and select the *Run* -> *Build & Start*. This approach can be faster if you have too many modules and you made change in one of the modules. + +### Running the Application + +Run the main application on ABP Studio, manually type `/Orders` to end of your application's URL to open the *Orders* page: + +![abp-studio-solution-runner-orders-page](images/abp-studio-solution-runner-orders-page.png) + +Great! We can see the list of orders. However, there are two problems: + +1. We don't see the Orders item on the main menu. This is because we haven't configured the [navigation menu system](../../framework/ui/mvc-razor-pages/navigation-menu.md) yet. +2. We see Product's GUID ID instead of its name. This is because the Ordering module has no integration with the Products module and doesn't have access to Product module's database to perform a JOIN query. + +We will solve the second problem in the [next part](part-06.md), but we can easily add a menu item for the Orders page now. + +### Adding a Menu Item + +ABP provides a modular navigation [menu system](../../framework/ui/mvc-razor-pages/navigation-menu.md) where each module can contribute to the main menu dynamically. + +Open the `ModularCrm.Ordering` .NET solution in your IDE and add the following `OrderingMenuContributor` class into the `ModularCrm.Ordering` project: + +````csharp +using System.Threading.Tasks; +using Volo.Abp.UI.Navigation; + +namespace ModularCrm.Ordering +{ + public class OrderingMenuContributor : IMenuContributor + { + public Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + context.Menu.AddItem( + new ApplicationMenuItem( + "ModularCrm.Orders.Index", // Unique menu id + "Orders", // Menu display text + "~/Orders", // URL + "fa-solid fa-basket-shopping" // Icon CSS class + ) + ); + } + + return Task.CompletedTask; + } + } +} +```` + +`OrderingMenuContributor` implements the `IMenuContributor` interface which forces us to implement the `ConfigureMenuAsync` method. In that method, we can just manipulate the menu items (add new menu items, remove existing menu items or change properties of existing menu items). The `ConfigureMenuAsync` method is executed whenever the menu is rendered on the UI, so you can dynamically decide how to manipulate the menu items. + +After creating such a class, we should configure the `AbpNavigationOptions` to add that contributor. Open the `OrderingWebModule` class in the `ModularCrm.Ordering` project and add the following configuration code into the `ConfigureServices` method (if there is no `ConfigureServices` method, first create it as shown below): + +````csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + options.MenuContributors.Add(new OrderingMenuContributor()); + }); +} +```` + +That's all. You can stop the main application (if it is already working), make a graph build on the main application, run it again on ABP Studio's *Solution Runner* panel and *Browse* it to see the result: + +![abp-studio-browser-orders-menu-item](images/abp-studio-browser-orders-menu-item.png) + +The *Orders* menu item is added under the *Products* menu item. + +> You can check the [menu documentation](../../framework/ui/mvc-razor-pages/navigation-menu.md) to learn more about manipulating menu items. + +## Summary + +In this part of the *Modular CRM* tutorial, we've built the functionality inside the Ordering module that we'd created in the [previous part](part-04.md). Since we've created the Ordering module from scratch (with the *Empty Module* template), we had to implement many aspects manually, added ABP packages, created some configuration classes, etc. It is good to do all these manually for one time for learning the things, but it is better to use the other module templates (that pre-configures the fundamentals for us) for a more comfortable development experience. + +In the next part, we will work for establishing communication between the Orders module and the Products module. \ No newline at end of file diff --git a/docs/en/tutorials/modular-crm/part-06.md b/docs/en/tutorials/modular-crm/part-06.md new file mode 100644 index 00000000000..1adfc052841 --- /dev/null +++ b/docs/en/tutorials/modular-crm/part-06.md @@ -0,0 +1,672 @@ +# Integrating the Modules + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Building the Ordering module", + "Path": "tutorials/modular-crm/part-05" + } +} +```` + +In the previous parts, we've created two modules: The Products module to store and manage products and the Orders module to accept orders. However, these modules were completely independent from each other. Only the main application brings them together to execute in the same application, but these modules don't communicate to each other. + +In this part, you will learn to implement three patterns for integrating these modules: + +1. The Order module will make request to the Products module to get product information when needed. +2. The Product module will listen to events from the Orders module, so it can decrease stock count of a product when an order is placed. +3. Finally, we will execute a database query that includes product and order data together. + +Let's begin from the first one... + +## Implementing Integration Services + +Remember from the [previous part](part-05.md), the Orders page shows products' identities instead of their names: + +![abp-studio-solution-runner-orders-page](images/abp-studio-solution-runner-orders-page.png) + +That is because the Orders module has no access to the product data, so it can not perform a JOIN query to get the names of products from the `Products` table. That is a natural result of the modular design. However, we also don't want to show a product's identity on the UI, which is not a good user experience. + +As a solution to that problem, the Orders module may ask product names to the Product module using an [integration service](../../framework/api-development/integration-services.md). Integration service concept in ABP is designed for request/response style inter-module (in modular applications) and inter-microservice (in distributed systems) communication. + +> When you implement integration services for inter-module communication, you can easily convert them to REST API calls if you convert your solution to a microservice system and convert your modules to services later. + +### Creating a Products Integration Service + +The first step is to create an integration service in the Products module, so other modules can consume it. + +We will define an interface in the `ModularCrm.Products.Application.Contracts` package and implement it in the `ModularCrm.Products.Application` package. + +#### Defining the `IProductIntegrationService` Interface + +Open the `ModularCrm.Products` .NET solution in your IDE, find the `ModularCrm.Products.Application.Contracts` project, create an `Integration` folder inside inside of that project and finally create an interface named `IProductIntegrationService` into that folder. The final folder structure should be like that: + +![visual-studio-product-integration-service](images/visual-studio-product-integration-service.png) + +(Creating an`Integration` folder is not required, but it can be a good practice) + +Open the `IProductIntegrationService.cs` file and replace it's content with the following code block: + +````csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Application.Services; + +namespace ModularCrm.Products.Integration +{ + [IntegrationService] + public interface IProductIntegrationService : IApplicationService + { + Task> GetProductsByIdsAsync(List ids); + } +} +```` + +`IProductIntegrationService` is very similar to a typical [application service](../../framework/architecture/domain-driven-design/application-services.md). The only difference is that it has an `[IntegrationService]` attribute on top of the interface definition. In that way, ABP can recognize and behave differently for them (for example, ABP doesn't expose [integration services](../../framework/api-development/integration-services.md) as HTTP APIs by default if you've configured the *[Auto API Controllers](../../framework/api-development/auto-controllers.md)* feature) + +`IProductIntegrationService` has a single method that gets a list of product Ids and returns a list of `ProductDto` objects for these Ids. Any other module can use that method to get details of products when it only has some product Ids. This is exactly what we need in the Ordering module. + +> **Design Tip** +> +> You may think if we can use the existing application services (like `IProductAppService`) from other modules instead of creating specific integration services. Technically you can use, ABP has no restriction. However, from good design and best practice points, we don't suggest it. Because, application services are designed to be consumed specifically by the presentation layer. They will have different authorization and validation logic, they will need different DTO input and output properties, they will have different performance, optimization and caching requirements, and so on. And most importantly, all these will change by the time based on UI requirements and these changes may break your integrations later. It is best to implement specific integration APIs that is designed and optimized for that purpose. +> +> We've reused the `ProductDto` object that was actually created for `IProductAppService`. That can be reasonable from the maintenance point. But, if you think your integration service results can be different than the application service results in the future, it can be good to separate them from the first day, so you don't need to introduce breaking changes later. + +#### Implementing the `ProductIntegrationService` Class + +We've defined the integration service interface. Now, we can implement it in the `ModularCrm.Products.Application` project. Create an `Integration` folder and then create a `ProductIntegrationService` class in that folder. The final folder structure should be like that: + +![visual-studio-product-integration-service-implementation](images/visual-studio-product-integration-service-implementation.png) + +Open the `ProductIntegrationService.cs` file and replace it's content with the following code block: + +````csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Domain.Repositories; + +namespace ModularCrm.Products.Integration +{ + [IntegrationService] + public class ProductIntegrationService + : ProductsAppService, IProductIntegrationService + { + private readonly IRepository _productRepository; + + public ProductIntegrationService(IRepository productRepository) + { + _productRepository = productRepository; + } + + public async Task> GetProductsByIdsAsync(List ids) + { + var products = await _productRepository.GetListAsync( + product => ids.Contains(product.Id) + ); + + return ObjectMapper.Map, List>(products); + } + } +} +```` + +The implementation is pretty simple. Just using a [repository](../../framework/architecture/domain-driven-design/repositories.md) to query `Product` [entities](../../framework/architecture/domain-driven-design/entities.md). + +> Here, we directly used `List` classes, but instead you could wrap inputs and outputs into [DTOs](../../framework/architecture/domain-driven-design/data-transfer-objects.md). In that way, it can be possible to add new properties to these DTOs without changing the signature of your integration service method (so, without introducing breaking change for your client modules). + +### Consuming the Products Integration Service + +The Product Integration Service is ready to be consumed by the other modules. In this section, we will use it in the Ordering module to convert product Ids to product names. + +#### Adding a Reference to the `ModularCrm.Products.Application.Contracts` Package + +Open the ABP Studio UI and stop the application if it is already running. Then open the *Solution Explorer* in ABP Studio, right-click the `ModularCrm.Ordering` package and select the *Add Package Reference* command: + +![abp-studio-add-package-reference-4](images/abp-studio-add-package-reference-4.png) + +In the opening dialog, select the *This solution* tab, find and check the `ModularCrm.Products.Application.Contracts` package and click the OK button: + +![abp-studio-add-package-reference-dialog-3](images/abp-studio-add-package-reference-dialog-3.png) + +ABP Studio adds the package reference and also arranges the [module](../../framework/architecture/modularity/basics.md) dependency. + +> Instead of directly adding such a package reference, it can be best to import the module first (right-click the `ModularCrm.Ordering` module, select the Import Module command and import the `ModularCrm.Products` module), then install the package reference. In that way, it would be easy to see and keep track of inter-module dependencies. + +#### Using the Products Integration Service + +Now, we can inject and use `IProductIntegrationService` in the Ordering module codebase. + +Open the `IndexModel` class (the `IndexModel.cshtml.cs` file under the `Pages/Orders` folder of the `ModularCrm.Ordering` project of the `ModularCrm.Ordering` .NET solution) and change its content as like the following code block: + +````csharp +using Microsoft.AspNetCore.Mvc.RazorPages; +using ModularCrm.Ordering.Entities; +using ModularCrm.Products.Integration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace ModularCrm.Ordering.Pages.Orders +{ + public class IndexModel : PageModel + { + public List Orders { get; set; } + + // Define a dictionary for Id -> Name conversion + public Dictionary ProductNames { get; set; } + + private readonly IRepository _orderRepository; + private readonly IProductIntegrationService _productIntegrationService; + + public IndexModel( + IRepository orderRepository, + IProductIntegrationService productIntegrationService) + { + _orderRepository = orderRepository; + _productIntegrationService = productIntegrationService; + } + + public async Task OnGetAsync() + { + // Getting the orders from this module's database + Orders = await _orderRepository.GetListAsync(); + + // Prepare a list of products we need + var productIds = Orders.Select(o => o.ProductId).Distinct().ToList(); + + // Request the related products from the product integration service + var products = await _productIntegrationService + .GetProductsByIdsAsync(productIds); + + // Create a dictionary to get a product name easily by its id + ProductNames = products.ToDictionary(p => p.Id, p => p.Name); + } + } +} +```` + +Let's see what we've changed: + +* Defined a `ProductNames` dictionary. We will use it on the UI to convert product ids to product names. We are filling that dictionary by getting products from the product integration service. +* Injecting `IProductIntegrationService` interface, so we can use it to request products. +* In the `OnGetAsync` method; + * First getting the orders from the ordering module's database just like done before. + * Next, we are preparing a unique list of product ids, since the `GetProductsByIdsAsync` methods requests it. + * Then we are calling the `IProductIntegrationService.GetProductsByIdsAsync` method to get a `List` object. + * In the last line, we are converting the product list to a dictionary where the key is `Guid Id` and the value is `string Name`. In that way, we can easily find a product's name with it's id. + +Open the `Index.cshtml` file, change the `@order.ProductId` part by `@Model.ProductNames[order.ProductId]` to write the product name instead of the product id. The final `Index.cshtml` content should be the following: + +````html +@page +@model ModularCrm.Ordering.Pages.Orders.IndexModel + +

Orders

+ + + + + @foreach (var order in Model.Orders) + { + + Customer: @order.CustomerName
+ Product: @Model.ProductNames[order.ProductId]
+ State: @order.State +
+ } +
+
+
+```` + +That's all. Now, you can graph build the main application and run in ABP Studio to see the result: + +![abp-studio-browser-list-of-orders-with-product-name](images/abp-studio-browser-list-of-orders-with-product-name.png) + +As you notice, we can see the product names instead of product ids. + +In the way explained in this section, you can easily create integration services for your modules and consume these integration services in any other module. + +> **Design Tip** +> +> It is suggested to keep that type of communication minimum to not couple your modules to each other. It can make your solution complicated and may also decrease your system performance. When you need to do it, think about performance and try to make some optimizations. For example, if the Ordering module frequently needs to product data, you can use a kind of [cache layer](../../framework/fundamentals/caching.md), so it doesn't make frequent requests to the Products module. Especially, if you consider to convert your system to a microservice solution in the future, too many direct integration API calls can be a performance bottleneck. + +## Communication via Messages (Events) + +Another common approach to communicate between modules is messaging. By publishing and handling messages, a module can perform operation when an event happens in another module. + +ABP provides two types of event buses for loosely coupled communication: + +* [Local Event Bus](https://abp.io/docs/latest/framework/infrastructure/event-bus/local) is suitable for in-process messaging. Since in a modular monolith, both of publisher and subscriber are in the same process, they can communicate in-process, without needing an external message broker. +* **[Distributed Event Bus](https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed)** is normal for inter-process messaging, like microservices, for publishing and subscribing to distributed events. However, ABP's distributed event bus works as local (in-process) by default (actually, it uses the Local Event Bus under the hood by default), unless you configure an external message broker. + +If you consider to convert your modular monolith to a microservice system later, it is best to use the Distributed Event Bus with default local/in-process implementation. It already supports database level transactional event execution and has no performance penalty. If you switch to an external provider (e.g. [RabbitMQ](../../framework/infrastructure/event-bus/distributed/rabbitmq.md) or [Kafka](../../framework/infrastructure/event-bus/distributed/kafka.md)), you don't need to change your application code. + +On the other hand, if you want to publish events and always subscribe in the same module, you should use the Local Event Bus. In that way, if you switch to microservices later, you don't accidently (and unnecessarily) make a local event distributed. Both of the event bus types can be used in the same system, just understand these and use them properly. + +Since we will use messaging (events) between different modules, we will use the distributed event bus. + +### Publishing an Event + +In the example scenario, we want to publish an event when a new order is placed. The Ordering module will publish the event since it knows when a new order is placed. The Products module will subscribe to that event and get notified when a new order is placed. It will decrease the stock count of the product that is related to the new order. The scenario is pretty simple, let's implement it. + +#### Defining the Event Class + +Open the `ModularCrm.Ordering` module in your IDE, find the `ModularCrm.Ordering.Contracts` project, create an `Events` folder and create an `OrderPlacedEto` class inside that folder. The final folder structure should be like that: + +![visual-studio-order-event](images/visual-studio-order-event.png) + +We've placed the `OrderPlacedEto` class inside the `ModularCrm.Ordering.Contracts` project since that project can be referenced and used by other modules without accessing internal implementation of the Ordering module. The `OrderPlacedEto` class definition should be the following: + +````csharp +using System; + +namespace ModularCrm.Ordering.Contracts.Events +{ + public class OrderPlacedEto + { + public string CustomerName { get; set; } + public Guid ProductId { get; set; } + } +} +```` + +`OrderPlacedEto` is very simple. It is a plain C# class and used to transfer data related to the event (*ETO* is an acronym for *Event Transfer Object*, a suggested naming convention, but not required). You can add more properties if it is needed. For this tutorial, it is more than enough. + +#### Using the `IDistributedEventBus` Service + +`IDistributedEventBus` service is used to publish events to the event bus. Until this point, the Ordering module has no functionality to create a new order. + +In the Part 3, we had used ABP's Auto HTTP API Controller feature to automatically expose HTTP APIs from application services. In this section, we will create an ASP.NET Core API controller class to create a new order. In that way, you will also see that it is not different than creating a regular ASP.NET Core controller. + +Open the `ModularCrm.Ordering` module's .NET solution, create a `Controllers` folder in the `ModularCrm.Ordering` project and place a controller class named `OrdersController` in that new folder. The final folder structure should be like that: + +![visual-studio-ordering-controller](images/visual-studio-ordering-controller.png) + +Here the full `OrdersController` class: + +````csharp +using Microsoft.AspNetCore.Mvc; +using ModularCrm.Ordering.Contracts.Enums; +using ModularCrm.Ordering.Contracts.Events; +using ModularCrm.Ordering.Entities; +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.EventBus.Distributed; + +namespace ModularCrm.Ordering.Controllers +{ + [Route("api/orders")] + [ApiController] + public class OrdersController : AbpControllerBase + { + private readonly IRepository _orderRepository; + private readonly IDistributedEventBus _distributedEventBus; + + public OrdersController( + IRepository orderRepository, + IDistributedEventBus distributedEventBus) + { + _orderRepository = orderRepository; + _distributedEventBus = distributedEventBus; + } + + [HttpPost] + public async Task CreateAsync(OrderCreationModel input) + { + // Create a new Order entity + var order = new Order + { + CustomerName = input.CustomerName, + ProductId = input.ProductId, + State = OrderState.Placed + }; + + // Save it to the database + await _orderRepository.InsertAsync(order); + + // Publish an event, so other modules can be informed + await _distributedEventBus.PublishAsync( + new OrderPlacedEto + { + ProductId = order.ProductId, + CustomerName = order.CustomerName + }); + + return Created(); + } + + public class OrderCreationModel + { + public Guid ProductId { get; set; } + + [Required] + [StringLength(120)] + public string CustomerName { get; set; } + } + } +} +```` + +The `OrdersController.CreateAsync` method simply creates a new `Order` entity, saves it to the database and finally publishes an `OrderPlacedEto` event. + +### Subscribing to an Event + +In this section, we will subscribe to the `OrderPlacedEto` event in the Products module and decrease the related product's stock count once a new order is placed. + +#### Adding a Reference to the `ModularCrm.Ordering.Contracts` Package + +Since the `OrderPlacedEto` class is located inside the `ModularCrm.Ordering.Contracts` project, we need to add that package's reference to the Products module. This time, we will use the *Import Module* feature of ABP Studio (as an alternative to approach we used in the *Adding a Reference to the `ModularCrm.Products.Application.Contracts` Package* section). + +Open the ABP Studio UI and stop the application if it is already running. Then open the *Solution Explorer* in ABP Studio, right-click the `ModularCrm.Products` module and select the *Import Module* command: + +![abp-studio-import-module-ordering](images/abp-studio-import-module-ordering.png) + +In the opening dialog, find and select the `ModularCrm.Ordering` module, check the *Install this module* option and click the OK button: + +![abp-studio-import-module-dialog-for-ordering](images/abp-studio-import-module-dialog-for-ordering.png) + +Once you click the OK button, the Ordering module is imported to the Products module and an installation dialog is open: + +![abp-studio-install-module-dialog-for-ordering](images/abp-studio-install-module-dialog-for-ordering.png) + +Here, select the `ModularCrm.Ordering.Contracts` package on the left side (because we want to add that package reference) and `ModularCrm.Products.Domain` package on the middle area (because we want to add the package reference to that project). We installed it to the [domain layer](../../framework/architecture/domain-driven-design/domain-layer.md) of the Products module since we will create our event handler into that layer. Click the OK button to finish the installation operation. + +You can check the ABP Studio's *Solution Explorer* panel to see the module import and the project reference (dependency). + +![abp-studio-imports-and-dependencies](images/abp-studio-imports-and-dependencies.png) + +#### Handling the `OrderPlacedEto` Event + +Now, it is possible to use the `OrderPlacedEto` class inside the Product module's domain layer since it has the `ModularCrm.Ordering.Contracts` package reference. + +Open the Product module's .NET solution in your IDE, locate the `ModularCrm.Products.Domain` project, create a new `Orders` folder and an `OrderEventHandler` class inside that folder. The final folder structure should be like that: + +![visual-studio-order-event-handler](images/visual-studio-order-event-handler.png) + +Replace the `OrderEventHandler.cs` file's content with the following code block: + +````csharp +using ModularCrm.Ordering.Contracts.Events; +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.EventBus.Distributed; + +namespace ModularCrm.Products.Orders +{ + public class OrderEventHandler : + IDistributedEventHandler, + ITransientDependency + { + private readonly IRepository _productRepository; + + public OrderEventHandler(IRepository productRepository) + { + _productRepository = productRepository; + } + + public async Task HandleEventAsync(OrderPlacedEto eventData) + { + // Find the related product + var product = await _productRepository.FindAsync(eventData.ProductId); + if (product == null) + { + return; + } + + // Decrease the stock count + product.StockCount = product.StockCount - 1; + + // Update the entity in the database + await _productRepository.UpdateAsync(product); + } + } +} +```` + +`OrderEventHandler` implements the `IDistributedEventHandler` interface. In that way, ABP recognizes that class and subscribes to the related event automatically. Implementing `ITransientDependency` simply registers the `OrderEventHandler` class to the [dependency injection](../../framework/fundamentals/dependency-injection.md) system as a transient object. + +We are injecting the product repository and updating the stock count in the event handler method (`HandleEventAsync`). That's all. + +#### Testing the Order Creation + +We will not create a UI for creating an order, to keep this tutorial more focused. You can easily create a form to create an order on your user interface. We will test it just using the Swagger UI in this section. + +Graph build the `ModularCrm.Web` application, run it on the ABP Studio's *Solution Runner* panel and browse the application UI as demonstrated earlier. + +Once the application is running and ready, manually type `/swagger` to the end of the URL and press the ENTER key. You should see the Swagger UI that is used to discover and test your HTTP APIs: + +![abp-studio-swagger-create-order](images/abp-studio-swagger-create-order.png) + +Find the *Orders* API, click the *Try it out* button, enter a sample value the the *Request body*: + +````json +{ + "productId": "0fbf7dd0-d7e9-0d18-9214-3a14d9fa1b74", + "customerName": "David" +} +```` + +> **IMPORTANT:** Here, you should type a valid Product Id from the Products table of your database! + +Once you press the *Execute* button, a new order is created. At that point, you can check the `/Orders` page to see if the new order is shown on the UI, and check the `/Products` page to see if the related product's stock count has decreased. + +Here, sample screenshots from the Products and Orders pages: + +![products-orders-pages-crop](images/products-orders-pages-crop.png) + +We placed a new order for Product C. As a result, Product C's stock count has decreased from 55 to 54 and a new line is added to the Orders page. + +## Joining the Products and Orders data + +One of the essential purposes of modularity is to create modules those hide (encapsulate) their internal data and implementation details from the other modules. They communicate each other through well-defined [integration services](../../framework/api-development/integration-services.md) and [events](framework/infrastructure/event-bus/distributed). In that way, you can independently develop and change module implementations (even modules' database structures) from each other as long as you don't break these inter-module integration points. + +### The Problem + +In a non-modular application, accessing the related data is easy. You could just write a LINQ expression that joins `Orders` and `Products` database tables to get the data with a single database query. It would be easier to implement and executed with a good performance. + +On the other hand, it becomes harder to perform operations or get reports those requires to access data of multiple modules in a modular system. Remember the *Implementing Integration Services* section; We couldn't access the product data inside the Ordering module (`IOrderingDbContext` only defines a `DbSet`), so we needed to create an integration services just to get names of products. This approach is harder to implement and less performant (yet it is acceptable if you don't show too many orders on the UI or if you properly implement a caching layer), but it gives freedom to the Products module about its internal database or application logic changes. For example, you can decide to move product data to another physical database, or even to another database management system (DBMS) without effecting the other modules. + +### A Solution Option + +If you want to perform a single database query that spans database tables of multiple modules in a modular system, you have still some options. One option can be creating a reporting module that has access to all of the entities (or database tables). However, when you do that, you accept the following limitations: + +* When you make changes in a module's database structure, you should also update your reporting code. That is reasonable, but you should be informed by all module developers in such a case. +* You can not change DBMS of a module easily. For example, if you decide to use MongoDB for your Products module while the Ordering module still uses SQL Server, performing such a JOIN operation would not be possible. Even moving Products module to another SQL Server database in another physical server can break your reporting logic. + +If these are not problems for you, or you can handle when they become problems, you can create reporting modules or aggregator modules that works with multiple modules' data. + +In the next section, we will use the main application's codebase to implement such a JOIN operation to keep the tutorial short. However, you already learned how to create new modules, so you can create a new module and develop your JOIN logic inside that new module if you want. + +### The Implementation + +In this section, we will create an application service in the main application's .NET solution. That application service will perform a LINQ operation on the `Product` and `Order` entities. + +#### Defining the Reporting Service Interface + +We will define the `IOrderReportingAppService` interface in the `ModularCrm.Application.Contracts` project of the main application's .NET solution. + +##### Adding `ModularCrm.Ordering.Contracts` Package Reference + +As the first step, we should to add a reference of the `ModularCrm.Ordering.Contracts` package (of the `ModularCrm.Ordering` module) since we will reuse the `OrderState` enum which is defined in that package. + +Open the ABP Studio's *Solution Explorer* panel, right-click the `ModularCrm.Application.Contracts` package and select the *Add Package Reference* command: + +![abp-studio-add-package-reference-5](images/abp-studio-add-package-reference-5.png) + +Select the *Imported modules* tab, find and check the `ModularCrm.Ordering.Contracts` package and click the OK button: + +![abp-studio-add-package-reference-dialog-4](images/abp-studio-add-package-reference-dialog-4.png) + +The package reference is added and now we can use the types in the `ModularCrm.Ordering.Contracts` package. + +##### Defining the `IOrderReportingAppService` Interface + +Open the main `ModularCrm` .NET solution in your IDE, find the `ModularCrm.Application.Contracts` project, create an `Orders` folder and add an `IOrderReportingAppService` interface inside it. Here the definition of that interface: + +````csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace ModularCrm.Orders +{ + public interface IOrderReportingAppService : IApplicationService + { + Task> GetLatestOrders(); + } +} +```` + +We have a single method, `GetLatestOrders`, that will return a list of the latest orders. We should also define the `OrderReportDto` class that is returned by that method. Create the following class in the same `Orders` folder: + +````csharp +using System; +using ModularCrm.Ordering.Contracts.Enums; + +namespace ModularCrm.Orders +{ + public class OrderReportDto + { + // Order data + public Guid OrderId { get; set; } + public string CustomerName { get; set; } + public OrderState State { get; set; } + + // Product data + public Guid ProductId { get; set; } + public string ProductName { get; set; } + } +} +```` + +`OrderReportDto` contains data from both of `Order` and `Product` entities. We could use the `OrderState` since we have a reference to the package which defines that enum. + +After adding these files, the final folder structure should be like that: + +![visual-studio-order-reporting-app-service](images/visual-studio-order-reporting-app-service.png) + +#### Implementing the `OrderReportingAppService` Class + +Create an `Orders` folder inside of the `ModularCrm.Application` project and add a class named `OrderReportingAppService` inside it. The final folder structure should be like that: + +![visual-studio-order-reporting-app-service-impl](images/visual-studio-order-reporting-app-service-impl.png) + +Open the `OrderReportingAppService.cs` file and change its content by the following code block: + +````csharp +using ModularCrm.Ordering.Entities; +using ModularCrm.Products; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace ModularCrm.Orders +{ + public class OrderReportingAppService : + ModularCrmAppService, + IOrderReportingAppService + { + private readonly IRepository _orderRepository; + private readonly IRepository _productRepository; + + public OrderReportingAppService( + IRepository orderRepository, + IRepository productRepository) + { + _orderRepository = orderRepository; + _productRepository = productRepository; + } + + public async Task> GetLatestOrders() + { + var orders = await _orderRepository.GetQueryableAsync(); + var products = await _productRepository.GetQueryableAsync(); + + var latestOrders = (from order in orders + join product in products on order.ProductId equals product.Id + orderby order.CreationTime descending + select new OrderReportDto + { + OrderId = order.Id, + CustomerName = order.CustomerName, + State = order.State, + ProductId = product.Id, + ProductName = product.Name + }) + .Take(10) + .ToList(); + + return latestOrders; + } + } +} +```` + +Let's explain that class; + +* It inject [repository](../../framework/architecture/domain-driven-design/repositories.md) services for `Order` and `Product` entities. We can access all entities of all modules from the main application codebase. +* In the `GetLatestOrders` method, we are getting `IQueryable` objects for the entities, so we can create LINQ expressions. +* Then we are executing a LINQ expression with `join` keyword, so we can be able to execute a single query that uses multiple tables. + +That's all. In that way, you can execute JOIN queries that uses multiple modules' data. However, if you write majority of your application code into the main application and perform operations on multiple modules, then your system won't be so modular. So, here we show it is technically possible. Please use at your own risk. + +#### Testing the Reporting Service + +We haven't created a UI to show list of the latest orders using `OrderReportingAppService`. However, we can use the Swagger UI again to test it. + +Open the ABP Studio UI, stop the application if it is running, build and run it again. Once the application starts, browse it, then add `/swagger` to the end of the URL to open the Swagger UI: + +![abp-studio-swagger-list-orders](images/abp-studio-swagger-list-orders.png) + +Here, find the `OrderReporting` API and execute it as shown above. You should get the order objects with product names. + +Alternatively, you can visit the `/api/app/order-reporting/latest-orders` URL to directly execute the HTTP API on the browser (you should write the full URL, like `https://localhost:44358/api/app/order-reporting/latest-orders` - port can be different for your case) + +## Summary + +In this part of the modular application development tutorial, you have learned three ways of integrating your application modules: + +1. You can use the integration services to make request/response style communication between your modules. +2. You can publish events from a module and subscribe to these events from other modules. +3. You can write your code into the main application, so you can access to all entities (and related data) of all modules. Instead of writing it into the main application code, you can also create some aggregation or reporting modules that can access more than one module entities. + +Now, you know the fundamental principles and mechanics to build sophisticated modular monolith application with ABP. + +## Download the Source Code + +You can download the completed sample solution [here](https://github.com/abpframework/abp-samples/tree/master/ModularCRM). + +## See Also + +See the following sections for additional resources. + +### The Book Store Tutorial + +In this tutorial, we intentionally kept the application logic very simple and didn't build a usable user interface for the modules. Also, didn't implement authorization and localization for the modules. This was to keep your focus on modularity. If you want to learn how to build real-world user interfaces with ABP, you can check the [Book Store tutorial](../book-store/index.md). All the principles and approaches explained there are already possible with a modular system too. + +### ABP Reusable Application Modules + +ABP is designed as modular from the first day. The ABP team has created tens of production-ready and [reusable application modules](../../modules/index.md). You can investigate these modules (some of them are already [free and open source](https://github.com/abpframework/abp/tree/dev/modules)) to see real-world modules. + +When you [create a new ABP solution](../../get-started/index.md), some of these modules are already comes as installed into your application (as NuGet and NPM packages). So, your initial ABP application is already a modular application from day one. + +### Guide: Module Development Best Practices & Conventions + +ABP team has created the [reusable application modules](../../modules/index.md) based on some strict principles and rules to make them reusable as much as possible in different scenarios, including modular monolith applications and microservice systems. + +These application modules are designed so that they are generic, extensible, doesn't depend on each other, reusable in any web application, provides multiple UI and database options, etc. In a monolith modular application, you mostly don't have such requirements. But, you can still check the [best practices guides](../../framework/architecture/best-practices/index.md) that is prepared by the ABP team and followed while implementing these application modules.