Browse Source

Merge branch 'dev' into auto-merge/rel-5-0/755

pull/11137/head
maliming 4 years ago
committed by GitHub
parent
commit
9ee77bc3ea
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 137
      .editorconfig
  2. 2
      .gitignore
  3. 6
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en-GB.json
  4. 7
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
  5. 6
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/tr.json
  6. 6
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en-GB.json
  7. 16
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json
  8. 6
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/tr.json
  9. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json
  10. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de-DE.json
  11. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en-GB.json
  12. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
  13. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json
  14. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fi.json
  15. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fr.json
  16. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hi.json
  17. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/is.json
  18. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/it.json
  19. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ro-RO.json
  20. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sk.json
  21. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json
  22. 2
      common.props
  23. 18
      docs/en/Blog-Posts/2021-11-18 v5_0_Preview/POST.md
  24. BIN
      docs/en/Blog-Posts/2021-11-18 v5_0_Preview/community-talks.png
  25. BIN
      docs/en/Blog-Posts/2021-11-18 v5_0_Preview/cover-50.png
  26. 47
      docs/en/Blog-Posts/2021-12-15 v5_0_Release_Stable/POST.md
  27. BIN
      docs/en/Blog-Posts/2021-12-15 v5_0_Release_Stable/community-talks.png
  28. 12
      docs/en/CLI.md
  29. 17
      docs/en/Community-Articles/2021-06-17-Using-Elsa-Workflow-with-ABP-Framework/POST.md
  30. 1671
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/POST.md
  31. BIN
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/application-contracts-folder-structure.png
  32. BIN
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/application-final-demo.gif
  33. BIN
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/application-folder-structure.png
  34. BIN
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/book-create.gif
  35. BIN
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/book-creation-modal.png
  36. BIN
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/book-update-modal.png
  37. BIN
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/demo.png
  38. BIN
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/domain-file-structure.png
  39. BIN
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/domain-shared-file-structure.png
  40. BIN
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/er-diagram.png
  41. BIN
      docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/web-folder-structure.png
  42. 321
      docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/POST.md
  43. BIN
      docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/calendar-component.png
  44. BIN
      docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/component-folder-structure.png
  45. BIN
      docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/copy-license-key.png
  46. BIN
      docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/manage-trial-1.png
  47. BIN
      docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/manage-trial-2.png
  48. BIN
      docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/syncfusion-libs.png
  49. BIN
      docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/syncfusion-package.png
  50. 2
      docs/en/Customizing-Application-Modules-Extending-Entities.md
  51. 2
      docs/en/Getting-Started-Running-Solution.md
  52. 2
      docs/en/Getting-Started.md
  53. 2
      docs/en/Tutorials/Part-2.md
  54. 2
      docs/en/Tutorials/Part-5.md
  55. 2
      docs/en/Virtual-File-System.md
  56. 1
      docs/zh-Hans/Background-Workers.md
  57. 11
      docs/zh-Hans/CLI.md
  58. 94
      docs/zh-Hans/Getting-Started-AspNetCore-Application.md
  59. 73
      docs/zh-Hans/Getting-Started-Create-Solution.md
  60. 199
      docs/zh-Hans/Getting-Started-Running-Solution.md
  61. 53
      docs/zh-Hans/Getting-Started-Setup-Environment.md
  62. 370
      docs/zh-Hans/Getting-Started.md
  63. 310
      docs/zh-Hans/Modules/Identity.md
  64. 174
      docs/zh-Hans/Modules/IdentityServer.md
  65. 119
      docs/zh-Hans/Tutorials/Part-1.md
  66. 234
      docs/zh-Hans/Tutorials/Part-2.md
  67. 592
      docs/zh-Hans/Tutorials/Part-3.md
  68. 54
      docs/zh-Hans/Tutorials/Part-4.md
  69. 636
      docs/zh-Hans/Tutorials/Part-5.md
  70. 279
      docs/zh-Hans/Tutorials/Part-6.md
  71. 239
      docs/zh-Hans/Tutorials/Part-7.md
  72. BIN
      docs/zh-Hans/Tutorials/images/blazor-add-book-button.png
  73. BIN
      docs/zh-Hans/Tutorials/images/blazor-add-books-component.png
  74. BIN
      docs/zh-Hans/Tutorials/images/blazor-bookstore-book-list.png
  75. BIN
      docs/zh-Hans/Tutorials/images/blazor-delete-book-action.png
  76. BIN
      docs/zh-Hans/Tutorials/images/blazor-edit-book-action-2.png
  77. BIN
      docs/zh-Hans/Tutorials/images/blazor-edit-book-modal.png
  78. BIN
      docs/zh-Hans/Tutorials/images/blazor-menu-bookstore.png
  79. BIN
      docs/zh-Hans/Tutorials/images/blazor-new-book-modal.png
  80. BIN
      docs/zh-Hans/Tutorials/images/bookstore-efcore-migration-authors.png
  81. BIN
      docs/zh-Hans/Tutorials/images/bookstore-efcore-migration.png
  82. BIN
      docs/zh-Hans/Tutorials/images/generated-proxies-3.png
  83. BIN
      docs/zh-Hans/Tutorials/images/vs-run-without-iisexpress.png
  84. 18
      docs/zh-Hans/docs-nav.json
  85. BIN
      docs/zh-Hans/images/create-aspnet-core-application.png
  86. BIN
      docs/zh-Hans/images/identity-module-menu.png
  87. BIN
      docs/zh-Hans/images/identity-module-permissions.png
  88. BIN
      docs/zh-Hans/images/identity-module-roles.png
  89. BIN
      docs/zh-Hans/images/identity-module-users.png
  90. 11
      framework/src/Volo.Abp.ApiVersioning.Abstractions/Volo/Abp/ApiVersioning/AbpApiVersioningAbstractionsModule.cs
  91. 9
      framework/src/Volo.Abp.ApiVersioning.Abstractions/Volo/Abp/ApiVersioning/IRequestedApiVersion.cs
  92. 15
      framework/src/Volo.Abp.ApiVersioning.Abstractions/Volo/Abp/ApiVersioning/NullRequestedApiVersion.cs
  93. 25
      framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Microsoft/AspNetCore/Builder/ApplicationBuilderAbpJwtTokenMiddlewareExtension.cs
  94. 9
      framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo/Abp/AspNetCore/Authentication/JwtBearer/AbpAspNetCoreAuthenticationJwtBearerModule.cs
  95. 83
      framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Microsoft/AspNetCore/Authentication/OAuth/Claims/AbpClaimActionCollectionExtensions.cs
  96. 9
      framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Volo/Abp/AspNetCore/Authentication/OAuth/AbpAspNetCoreAuthenticationOAuthModule.cs
  97. 67
      framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Volo/Abp/AspNetCore/Authentication/OAuth/Claims/MultipleClaimAction.cs
  98. 39
      framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Volo/Abp/AspNetCore/Authentication/OAuth/Claims/RemoveDuplicateClaimAction.cs
  99. 81
      framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Microsoft/Extensions/DependencyInjection/AbpOpenIdConnectExtensions.cs
  100. 13
      framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Volo/Abp/AspNetCore/Authentication/OpenIdConnect/AbpAspNetCoreAuthenticationOpenIdConnectModule.cs

137
.editorconfig

@ -0,0 +1,137 @@
# EditorConfig is awesome: https://EditorConfig.org
# Please feel free to update it (by considering the code style of ABP Team).
# top-most EditorConfig file
root = true
[*.cs]
#Namespace settings
csharp_style_namespace_declarations = file_scoped
dotnet_diagnostic.IDE0161.severity = warning
#Core editorconfig formatting - indentation
#use tabs for indentation
indent_style = tabs
#Formatting - indentation options
#indent switch case contents.
csharp_indent_case_contents = true
#indent switch labels
csharp_indent_switch_labels = true
#Formatting - new line options
#place catch statements on a new line
csharp_new_line_before_catch = true
#place else statements on a new line
csharp_new_line_before_else = true
#require members of object intializers to be on separate lines
csharp_new_line_before_members_in_object_initializers = true
#require braces to be on a new line for object_collection_array_initializers, methods, control_blocks, types, and lambdas (also known as "Allman" style)
csharp_new_line_before_open_brace = object_collection_array_initializers, methods, control_blocks, types, lambdas
#Formatting - organize using options
#sort System.* using directives alphabetically, and place them before other usings
dotnet_sort_system_directives_first = true
#Formatting - spacing options
#require NO space between a cast and the value
csharp_space_after_cast = false
#require a space before the colon for bases or interfaces in a type declaration
csharp_space_after_colon_in_inheritance_clause = true
#require a space after a keyword in a control flow statement such as a for loop
csharp_space_after_keywords_in_control_flow_statements = true
#require a space before the colon for bases or interfaces in a type declaration
csharp_space_before_colon_in_inheritance_clause = true
#remove space within empty argument list parentheses
csharp_space_between_method_call_empty_parameter_list_parentheses = false
#remove space between method call name and opening parenthesis
csharp_space_between_method_call_name_and_opening_parenthesis = false
#do not place space characters after the opening parenthesis and before the closing parenthesis of a method call
csharp_space_between_method_call_parameter_list_parentheses = false
#remove space within empty parameter list parentheses for a method declaration
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
#place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list.
csharp_space_between_method_declaration_parameter_list_parentheses = false
#Formatting - wrapping options
#leave code block on single line
csharp_preserve_single_line_blocks = true
#Style - Code block preferences
#prefer curly braces even for one line of code
csharp_prefer_braces = true:suggestion
#Style - expression bodied member options
#prefer block bodies for constructors
csharp_style_expression_bodied_constructors = false:suggestion
#prefer block bodies for methods
csharp_style_expression_bodied_methods = false:suggestion
#prefer expression-bodied members for properties
csharp_style_expression_bodied_properties = true:suggestion
#Style - expression level options
#prefer out variables to be declared inline in the argument list of a method call when possible
csharp_style_inlined_variable_declaration = true:suggestion
#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them
dotnet_style_predefined_type_for_member_access = true:suggestion
#Style - Expression-level preferences
#prefer default over default(T)
csharp_prefer_simple_default_expression = true:suggestion
#prefer objects to be initialized using object initializers when possible
dotnet_style_object_initializer = true:suggestion
#prefer inferred tuple element names
dotnet_style_prefer_inferred_tuple_names = true:suggestion
#Style - implicit and explicit types
#prefer var over explicit type in all cases, unless overridden by another code style rule
csharp_style_var_elsewhere = true:suggestion
#prefer var is used to declare variables with built-in system types such as int
csharp_style_var_for_built_in_types = true:suggestion
#prefer var when the type is already mentioned on the right-hand side of a declaration expression
csharp_style_var_when_type_is_apparent = true:suggestion
#Style - language keyword and framework type options
#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
#Style - Miscellaneous preferences
#prefer local functions over anonymous functions
csharp_style_pattern_local_over_anonymous_function = true:suggestion
#Style - modifier options
#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods.
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
#Style - Modifier preferences
#when this rule is set to a list of modifiers, prefer the specified ordering.
csharp_preferred_modifier_order = public,protected,private,virtual,async,readonly,static,override,abstract:suggestion
#Style - Pattern matching
#prefer pattern matching instead of is expression with type casts
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
#Style - qualification options
#prefer fields not to be prefaced with this. or Me. in Visual Basic
dotnet_style_qualification_for_field = false:suggestion
#prefer methods not to be prefaced with this. or Me. in Visual Basic
dotnet_style_qualification_for_method = false:suggestion
#prefer properties not to be prefaced with this. or Me. in Visual Basic
dotnet_style_qualification_for_property = false:suggestion

2
.gitignore

@ -6,7 +6,7 @@
*.user *.user
*.userosscache *.userosscache
*.sln.docstates *.sln.docstates
*.editorconfig
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs *.userprefs

6
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en-GB.json

@ -210,6 +210,10 @@
"Completed": "Completed", "Completed": "Completed",
"Failed": "Failed", "Failed": "Failed",
"PaymentRequestDeletionWarningMessage": "This payment request will be deleted. Do you confirm that?", "PaymentRequestDeletionWarningMessage": "This payment request will be deleted. Do you confirm that?",
"Payment": "Payment" "Payment": "Payment",
"Permission:SendWelcomeEmail": "Send Welcome Email",
"SendWelcomeEmail": "Send Welcome Email",
"SendWelcomeEmailWarningMessage": "Are you sure you want to send welcome email to the organization members?",
"SendWelcomeEmailSuccessMessage": "Welcome email sent successfully!"
} }
} }

7
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json

@ -352,6 +352,11 @@
"Volo.AbpIo.Commercial:030008": "Purchase date can be set only when status is Purchased!", "Volo.AbpIo.Commercial:030008": "Purchase date can be set only when status is Purchased!",
"Volo.AbpIo.Commercial:030009": "User not found!", "Volo.AbpIo.Commercial:030009": "User not found!",
"Volo.AbpIo.Commercial:030010": "To purchase the trial license, first you need to activate your trial license!", "Volo.AbpIo.Commercial:030010": "To purchase the trial license, first you need to activate your trial license!",
"Volo.AbpIo.Commercial:030011": "You cannot delete a trial license when it is purchased!" "Volo.AbpIo.Commercial:030011": "You cannot delete a trial license when it is purchased!",
"Permission:SendWelcomeEmail": "Send Welcome Email",
"SendWelcomeEmail": "Send Welcome Email",
"SendWelcomeEmailWarningMessage": "Are you sure you want to send welcome email to the organization members?",
"SendWelcomeEmailSuccessMessage": "Welcome email sent successfully!",
"PaymentRequestId": "Payment Request Id"
} }
} }

6
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/tr.json

@ -348,6 +348,10 @@
"Volo.AbpIo.Commercial:030008": "Satın alma tarihi yalnızca durum Satın Alındı olduğunda ayarlanabilir!", "Volo.AbpIo.Commercial:030008": "Satın alma tarihi yalnızca durum Satın Alındı olduğunda ayarlanabilir!",
"Volo.AbpIo.Commercial:030009": "Kullanıcı bulunamadı!", "Volo.AbpIo.Commercial:030009": "Kullanıcı bulunamadı!",
"Volo.AbpIo.Commercial:030010": "Deneme lisansını satın almak için önce deneme lisansınızı etkinleştirmeniz gerekir!", "Volo.AbpIo.Commercial:030010": "Deneme lisansını satın almak için önce deneme lisansınızı etkinleştirmeniz gerekir!",
"Volo.AbpIo.Commercial:030011": "Satın alındığında bir deneme lisansını silemezsiniz!" "Volo.AbpIo.Commercial:030011": "Satın alındığında bir deneme lisansını silemezsiniz!",
"Permission:SendWelcomeEmail": "Hoşgeldin Emaili Gönder",
"SendWelcomeEmail": "Hoşgeldin Emaili Gönder",
"SendWelcomeEmailWarningMessage": "Organizasyon üyelerine hoşgeldin emaili göndermek istediğinden emin misin?",
"SendWelcomeEmailSuccessMessage": "Hoşgeldin emaili başarıyla gönderilmiştir!"
} }
} }

6
abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en-GB.json

@ -102,6 +102,10 @@
"InvalidFormInputs": "Please, type the valid information specified on the form.", "InvalidFormInputs": "Please, type the valid information specified on the form.",
"DDDBookEmailBody": "Thank you. <br /> To download your book, <a href=\"{0}\">click here</a>.", "DDDBookEmailBody": "Thank you. <br /> To download your book, <a href=\"{0}\">click here</a>.",
"FreeDDDEBook": "Free DDD E-Book", "FreeDDDEBook": "Free DDD E-Book",
"CommercialNewsletterConfirmationMessage": "I agree to the <a href=\"https://commercial.abp.io/TermsConditions\">Terms & Conditions</a> and <a href=\"https://commercial.abp.io/Privacy\">Privacy Policy</a>." "CommercialNewsletterConfirmationMessage": "I agree to the <a href=\"https://commercial.abp.io/TermsConditions\">Terms & Conditions</a> and <a href=\"https://commercial.abp.io/Privacy\">Privacy Policy</a>.",
"RemoveCurrentUserFromOrganizationWarningMessage": "You are removing yourself from your own organization. You will no longer be able to manage this organization, do you confirm?",
"RenewExistingOrganizationOrCreateNewOneMessage": "You can renew the license of your organization(s) by clicking the below \"Extend Now\" button(s) and thus you can extend the license expiry date by 1 year. If you continue to checkout, you will have a new organization. Do you want to continue with a new organization?",
"ExtendNow": "Extend Now",
"ContinueWithNewOrganization": "Continue with a new organization"
} }
} }

16
abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json

@ -178,7 +178,7 @@
"ChangingLicenseType": "Can I upgrade my license type later?", "ChangingLicenseType": "Can I upgrade my license type later?",
"ChangingLicenseTypeExplanation": "You can upgrade to a higher license by paying the difference within your active license period. When you upgrade to a higher license plan, you get the benefits of the new plan, but the license upgrade does not change the license expiry date. Besides, you can also add new developer seats to your existing license, see \"How many developers can work on the ABP Commercial?\"", "ChangingLicenseTypeExplanation": "You can upgrade to a higher license by paying the difference within your active license period. When you upgrade to a higher license plan, you get the benefits of the new plan, but the license upgrade does not change the license expiry date. Besides, you can also add new developer seats to your existing license, see \"How many developers can work on the ABP Commercial?\"",
"LicenseExtendUpgradeDiff": "What is the difference between license extend and upgrade?", "LicenseExtendUpgradeDiff": "What is the difference between license extend and upgrade?",
"LicenseExtendUpgradeDiffExplanation": "<strong>Extending:</strong> By extending/renewing your license, you will continue to get premium support and get major updates for the modules and themes. Besides, you will be able to continue creating new projects. And you will still be able to use ABP Suite which speeds up your development.<hr/><strong>Upgrading:</strong> By upgrading your license, you will promote to a higher license plan which will allow you to get additional benefits. See the <a href=\"/pricing\">license comparison table</a> to check the differences between the license plans.<strong>On the other hand, when you upgrade, your license expiry date will not change!</strong>To extend your license end date, you need to extend your license.", "LicenseExtendUpgradeDiffExplanation": "<strong>Extending:</strong> By extending/renewing your license, you will continue to get premium support and get major updates for the modules and themes. Besides, you will be able to continue creating new projects. And you will still be able to use ABP Suite which speeds up your development. When you extend your license, 1 year is added to your license expiry date. <hr/><strong>Upgrading:</strong> By upgrading your license, you will promote to a higher license plan which will allow you to get additional benefits. See the <a href=\"/pricing\">license comparison table</a> to check the differences between the license plans.<strong>On the other hand, when you upgrade, your license expiry date will not change!</strong>To extend your license end date, you need to extend your license.",
"LicenseRenewalCost": "What is the license renewal cost after 1 year?", "LicenseRenewalCost": "What is the license renewal cost after 1 year?",
"LicenseRenewalCostExplanation": "The renewal (extend) rate of all ABP Commercial perpetual licenses is {0} of the license list price. The renewal price of the standard Team License is ${1}, standard Business License is ${2} and standard Enterprise License is ${3}. If you are already a customer, <a href='{4}' target='_blank'>log into your account</a> to review the available renewal pricing.", "LicenseRenewalCostExplanation": "The renewal (extend) rate of all ABP Commercial perpetual licenses is {0} of the license list price. The renewal price of the standard Team License is ${1}, standard Business License is ${2} and standard Enterprise License is ${3}. If you are already a customer, <a href='{4}' target='_blank'>log into your account</a> to review the available renewal pricing.",
"HowDoIRenewMyLicense": "How do I renew my license?", "HowDoIRenewMyLicense": "How do I renew my license?",
@ -193,7 +193,7 @@
"WhatHappensWhenLicenseEnds": "What happens when my license period ends?", "WhatHappensWhenLicenseEnds": "What happens when my license period ends?",
"WhatHappensWhenLicenseEndsExplanation1": "ABP Commercial license type is <a href=\"{0}\" target=\"_blank\">perpetual license</a>. After your license expires, you can continue developing your project. And you are not obliged to renew your license. Your license comes with a one-year Updates and Support plan out of the box. To continue to receive new features, performance enhancements, bug fixes, support and continue to use ABP Suite, make sure to renew your plan each year. When your license expires, you will not be able to get more of the following benefits;", "WhatHappensWhenLicenseEndsExplanation1": "ABP Commercial license type is <a href=\"{0}\" target=\"_blank\">perpetual license</a>. After your license expires, you can continue developing your project. And you are not obliged to renew your license. Your license comes with a one-year Updates and Support plan out of the box. To continue to receive new features, performance enhancements, bug fixes, support and continue to use ABP Suite, make sure to renew your plan each year. When your license expires, you will not be able to get more of the following benefits;",
"WhatHappensWhenLicenseEndsExplanation2": "You can not create new solutions using the ABP Commercial, but you can continue to develop your existing applications forever.", "WhatHappensWhenLicenseEndsExplanation2": "You can not create new solutions using the ABP Commercial, but you can continue to develop your existing applications forever.",
"WhatHappensWhenLicenseEndsExplanation3": "You will be able to get updates for the modules and themes within your MAJOR version. For example; if you are using v3.2.0 of a module, you can still get updates for v3.x.x (v3.3.0, v3.5.2... etc.) of that module. But you cannot get updates for the next major version (like v4.x, v5.x)", "WhatHappensWhenLicenseEndsExplanation3": "You will be able to get updates for the modules and themes within your MAJOR version (except RC or Preview versions). For example; if you are using v3.2.0 of a module, you can still get updates for v3.x.x (v3.3.0, v3.5.2... etc.) of that module. But you cannot get updates for the next major version (like v4.x, v5.x). Let's say, when your license expired, the latest release was v4.4.3 and the latest preview version was v5.0.0-rc.2, you can only access the v4.X.X but you cannot access the v5.X.X.",
"WhatHappensWhenLicenseEndsExplanation4": "You can not install new modules and themes added to the ABP Commercial platform after your license ends.", "WhatHappensWhenLicenseEndsExplanation4": "You can not install new modules and themes added to the ABP Commercial platform after your license ends.",
"WhatHappensWhenLicenseEndsExplanation5": "You can not use the ABP Suite.", "WhatHappensWhenLicenseEndsExplanation5": "You can not use the ABP Suite.",
"WhatHappensWhenLicenseEndsExplanation6": "You can not get the <a href=\"{0}\">premium support</a> anymore.", "WhatHappensWhenLicenseEndsExplanation6": "You can not get the <a href=\"{0}\">premium support</a> anymore.",
@ -390,6 +390,16 @@
"TrialLicenseExpireMessage": "You are using the trial license and your trial license will expire on {0}.", "TrialLicenseExpireMessage": "You are using the trial license and your trial license will expire on {0}.",
"TryForFree": "Try For Free", "TryForFree": "Try For Free",
"TrialLicenseExpiredInfo": "Your trial license period has expired!", "TrialLicenseExpiredInfo": "Your trial license period has expired!",
"CommercialNewsletterConfirmationMessage": "I agree to the <a href=\"https://commercial.abp.io/TermsConditions\">Terms & Conditions</a> and <a href=\"https://commercial.abp.io/Privacy\">Privacy Policy</a>." "CommercialNewsletterConfirmationMessage": "I agree to the <a href=\"https://commercial.abp.io/TermsConditions\">Terms & Conditions</a> and <a href=\"https://commercial.abp.io/Privacy\">Privacy Policy</a>.",
"DowngradeLicensePlan": "Can I downgrade to a lower license plan in the future?",
"DowngradeLicensePlanExplanation": "You cannot downgrade your existing license plan. But you can purchase a new lower license plan and continue to your development on the new license. After you purchase a lower license, you just need to login to your new license plan via ABP CLI command: ` abp login <username> -o <organization> `.",
"LicenseTransfer": "Can a license be transferred from one developer to another?",
"LicenseTransferExplanation": "Yes! When you purchase a license, you become the license holder, hence you will have access to the organization management page. An organization has owner and developer roles. Owners can manage the developer seats and assign developers. Each assigned developer will login via ABP CLI command into the system and will have development and support permissions.",
"UserOwnerDescription": "The 'Owner' of the organization is the admin of this account. He/she manages the organization by purchasing licenses, allocating developers. An 'Owner' cannot write code in the ABP Commercial projects, cannot download the ABP sample projects, or cannot ask questions on the support website. If you want to do all these, you have to add yourself as a developer too.",
"UserDeveloperDescription": "The 'Developers' can write code in the ABP Commercial projects, download the ABP sample projects, or ask questions on the support website. On the other hand, the 'Developers' cannot manage this organization.",
"RemoveCurrentUserFromOrganizationWarningMessage": "You are removing yourself from your own organization. You will no longer be able to manage this organization, do you confirm?",
"RenewExistingOrganizationOrCreateNewOneMessage": "You can renew the license of your organization(s) by clicking the below \"Extend Now\" button(s) and thus you can extend the license expiry date by 1 year. If you continue to checkout, you will have a new organization. Do you want to continue with a new organization?",
"ExtendNow": "Extend Now",
"ContinueWithNewOrganization": "Continue with a new organization"
} }
} }

6
abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/tr.json

@ -390,6 +390,10 @@
"TrialLicenseExpireMessage": "Deneme lisansını kullanıyorsunuz ve deneme lisansınızın süresi {0} tarihinde sona erecek.", "TrialLicenseExpireMessage": "Deneme lisansını kullanıyorsunuz ve deneme lisansınızın süresi {0} tarihinde sona erecek.",
"TryForFree": "Ücretsiz deneyin", "TryForFree": "Ücretsiz deneyin",
"TrialLicenseExpiredInfo": "Deneme lisansınızın süresi doldu!", "TrialLicenseExpiredInfo": "Deneme lisansınızın süresi doldu!",
"CommercialNewsletterConfirmationMessage": "<a href=\"https://commercial.abp.io/TermsConditions\">Şartlar, Koşullar</a> ve <a href=\"https://commercial.abp.io/Privacy\">Gizlilik politikasını</a> kabul ediyorum." "CommercialNewsletterConfirmationMessage": "<a href=\"https://commercial.abp.io/TermsConditions\">Şartlar, Koşullar</a> ve <a href=\"https://commercial.abp.io/Privacy\">Gizlilik politikasını</a> kabul ediyorum.",
"RemoveCurrentUserFromOrganizationWarningMessage": "Organizasyondan kendinizi kaldırıyorsunuz. Artık bu organizasyonu yönetemeyeceksiniz, onaylıyor musunuz? ",
"RenewExistingOrganizationOrCreateNewOneMessage": "Aşağıdaki \"Şimdi Uzat\" buton(lar)una tıklayarak organizasyon(lar)ınızın lisansını yenileyebilir ve böylece lisans geçerlilik süresini 1 yıl uzatabilirsiniz. Ödemeye devam ederseniz, yeni bir organizasyonunuz olacak. Yeni bir organizasyonla devam etmek istiyor musunuz? ",
"ExtendNow": "Şimdi Uzat",
"ContinueWithNewOrganization": "Yeni bir organizasyon ile devam et"
} }
} }

3
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json

@ -142,6 +142,7 @@
"MinimumSearchContent": "يجب عليك إدخال 3 أحرف على الأقل!", "MinimumSearchContent": "يجب عليك إدخال 3 أحرف على الأقل!",
"Volo.AbpIo.Domain:060001": "عنوان URL المصدر (\"{ArticleUrl}\") ليس عنوان URL لـ Github", "Volo.AbpIo.Domain:060001": "عنوان URL المصدر (\"{ArticleUrl}\") ليس عنوان URL لـ Github",
"Volo.AbpIo.Domain:060002": "محتوى المقالة غير متوفر من مورد Github (\"{ArticleUrl}\").", "Volo.AbpIo.Domain:060002": "محتوى المقالة غير متوفر من مورد Github (\"{ArticleUrl}\").",
"Volo.AbpIo.Domain:060003": "لم يتم العثور على محتوى مقال!" "Volo.AbpIo.Domain:060003": "لم يتم العثور على محتوى مقال!",
"SeeMore": "شاهد المزيد"
} }
} }

3
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de-DE.json

@ -142,6 +142,7 @@
"MinimumSearchContent": "Sie müssen mindestens 3 Zeichen eingeben!", "MinimumSearchContent": "Sie müssen mindestens 3 Zeichen eingeben!",
"Volo.AbpIo.Domain:060001": "Quell-URL(\"{ArticleUrl}\") ist keine Github-URL", "Volo.AbpIo.Domain:060001": "Quell-URL(\"{ArticleUrl}\") ist keine Github-URL",
"Volo.AbpIo.Domain:060002": "Artikelinhalt ist über die Github(\"{ArticleUrl}\")-Ressource nicht verfügbar.", "Volo.AbpIo.Domain:060002": "Artikelinhalt ist über die Github(\"{ArticleUrl}\")-Ressource nicht verfügbar.",
"Volo.AbpIo.Domain:060003": "Kein Artikelinhalt gefunden!" "Volo.AbpIo.Domain:060003": "Kein Artikelinhalt gefunden!",
"SeeMore": "Mehr Sehen"
} }
} }

3
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en-GB.json

@ -101,6 +101,7 @@
"ArticleRequestMessageTitle": "<a href=\"https://github.com/abpframework/abp/issues/new\">Open an issue</a> on the GitHub to request an article/tutorial you want to see on this web site.", "ArticleRequestMessageTitle": "<a href=\"https://github.com/abpframework/abp/issues/new\">Open an issue</a> on the GitHub to request an article/tutorial you want to see on this web site.",
"ArticleRequestMessageBody": "Here is the list of the requested articles by the Community. Do you want to write a requested article? Please click on the request and join the discussion.", "ArticleRequestMessageBody": "Here is the list of the requested articles by the Community. Do you want to write a requested article? Please click on the request and join the discussion.",
"Language": "Language", "Language": "Language",
"CreateArticleLanguageInfo": "The language in which the article is written" "CreateArticleLanguageInfo": "Language of the article",
"SeeMore": "See More"
} }
} }

3
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json

@ -142,6 +142,7 @@
"MinimumSearchContent": "You must enter at least 3 characters!", "MinimumSearchContent": "You must enter at least 3 characters!",
"Volo.AbpIo.Domain:060001": "Source URL(\"{ArticleUrl}\") is not Github URL", "Volo.AbpIo.Domain:060001": "Source URL(\"{ArticleUrl}\") is not Github URL",
"Volo.AbpIo.Domain:060002": "Article Content is not available from Github(\"{ArticleUrl}\") resource.", "Volo.AbpIo.Domain:060002": "Article Content is not available from Github(\"{ArticleUrl}\") resource.",
"Volo.AbpIo.Domain:060003": "No article content found!" "Volo.AbpIo.Domain:060003": "No article content found!",
"SeeMore": "See More"
} }
} }

3
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json

@ -142,6 +142,7 @@
"MinimumSearchContent": "¡Debes ingresar al menos 3 caracteres!", "MinimumSearchContent": "¡Debes ingresar al menos 3 caracteres!",
"Volo.AbpIo.Domain:060001": "La URL de origen (\"{ArticleUrl}\") no es la URL de Github", "Volo.AbpIo.Domain:060001": "La URL de origen (\"{ArticleUrl}\") no es la URL de Github",
"Volo.AbpIo.Domain:060002": "El contenido del artículo no está disponible en el recurso de Github (\"{ArticleUrl}\").", "Volo.AbpIo.Domain:060002": "El contenido del artículo no está disponible en el recurso de Github (\"{ArticleUrl}\").",
"Volo.AbpIo.Domain:060003": "¡No se encontró contenido del artículo!" "Volo.AbpIo.Domain:060003": "¡No se encontró contenido del artículo!",
"SeeMore": "Ver Más"
} }
} }

3
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fi.json

@ -142,6 +142,7 @@
"MinimumSearchContent": "Sinun on annettava vähintään 3 merkkiä!", "MinimumSearchContent": "Sinun on annettava vähintään 3 merkkiä!",
"Volo.AbpIo.Domain:060001": "Lähteen URL-osoite (\"{ArticleUrl}\") ei ole Githubin URL-osoite", "Volo.AbpIo.Domain:060001": "Lähteen URL-osoite (\"{ArticleUrl}\") ei ole Githubin URL-osoite",
"Volo.AbpIo.Domain:060002": "Artikkelin sisältö ei ole saatavilla Githubin (\"{ArticleUrl}\") -resurssista.", "Volo.AbpIo.Domain:060002": "Artikkelin sisältö ei ole saatavilla Githubin (\"{ArticleUrl}\") -resurssista.",
"Volo.AbpIo.Domain:060003": "Artikkelin sisältöä ei löytynyt!" "Volo.AbpIo.Domain:060003": "Artikkelin sisältöä ei löytynyt!",
"SeeMore": "Katso Lisää"
} }
} }

3
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fr.json

@ -142,6 +142,7 @@
"MinimumSearchContent": "Vous devez saisir au moins 3 caractères!", "MinimumSearchContent": "Vous devez saisir au moins 3 caractères!",
"Volo.AbpIo.Domain:060001": "L'URL source (\"{ArticleUrl}\") n'est pas une URL Github", "Volo.AbpIo.Domain:060001": "L'URL source (\"{ArticleUrl}\") n'est pas une URL Github",
"Volo.AbpIo.Domain:060002": "Le contenu de l'article n'est pas disponible à partir de la ressource Github(\"{ArticleUrl}\").", "Volo.AbpIo.Domain:060002": "Le contenu de l'article n'est pas disponible à partir de la ressource Github(\"{ArticleUrl}\").",
"Volo.AbpIo.Domain:060003": "Aucun contenu d'article trouvé !" "Volo.AbpIo.Domain:060003": "Aucun contenu d'article trouvé !",
"SeeMore": "Voir Plus"
} }
} }

3
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hi.json

@ -142,6 +142,7 @@
"MinimumSearchContent": "आपको कम से कम 3 वर्ण दर्ज करने होंगे!", "MinimumSearchContent": "आपको कम से कम 3 वर्ण दर्ज करने होंगे!",
"Volo.AbpIo.Domain:060001": "स्रोत URL (\"{ArticleUrl}\") जीथब URL नहीं है", "Volo.AbpIo.Domain:060001": "स्रोत URL (\"{ArticleUrl}\") जीथब URL नहीं है",
"Volo.AbpIo.Domain:060002": "लेख सामग्री Github (\"{ArticleUrl}\") संसाधन से उपलब्ध नहीं है।", "Volo.AbpIo.Domain:060002": "लेख सामग्री Github (\"{ArticleUrl}\") संसाधन से उपलब्ध नहीं है।",
"Volo.AbpIo.Domain:060003": "कोई लेख सामग्री नहीं मिली!" "Volo.AbpIo.Domain:060003": "कोई लेख सामग्री नहीं मिली!",
"SeeMore": "और देखें"
} }
} }

3
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/is.json

@ -142,6 +142,7 @@
"MinimumSearchContent": "Þú verður að slá inn að minnsta kosti 3 stafi!", "MinimumSearchContent": "Þú verður að slá inn að minnsta kosti 3 stafi!",
"Volo.AbpIo.Domain:060001": "Upprunaslóð (\"{ArticleUrl} \") er ekki Github slóð", "Volo.AbpIo.Domain:060001": "Upprunaslóð (\"{ArticleUrl} \") er ekki Github slóð",
"Volo.AbpIo.Domain:060002": "Innihald greinar er ekki fáanlegt frá Github (\"{ArticleUrl} \") resoursum.", "Volo.AbpIo.Domain:060002": "Innihald greinar er ekki fáanlegt frá Github (\"{ArticleUrl} \") resoursum.",
"Volo.AbpIo.Domain:060003": "Innihald greinar fannst ekki!" "Volo.AbpIo.Domain:060003": "Innihald greinar fannst ekki!",
"SeeMore": "Sjá Meira"
} }
} }

3
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/it.json

@ -142,6 +142,7 @@
"MinimumSearchContent": "Devi inserire almeno 3 caratteri!", "MinimumSearchContent": "Devi inserire almeno 3 caratteri!",
"Volo.AbpIo.Domain:060001": "Source URL(\"{ArticleUrl}\") non è un URL di GitHub", "Volo.AbpIo.Domain:060001": "Source URL(\"{ArticleUrl}\") non è un URL di GitHub",
"Volo.AbpIo.Domain:060002": "Il contenuto dell'articolo non è disponibile dalla risorsa Github(\"{ArticleUrl}\").", "Volo.AbpIo.Domain:060002": "Il contenuto dell'articolo non è disponibile dalla risorsa Github(\"{ArticleUrl}\").",
"Volo.AbpIo.Domain:060003": "Nessun contenuto dell'articolo trovato!" "Volo.AbpIo.Domain:060003": "Nessun contenuto dell'articolo trovato!",
"SeeMore": "Vedi Altro"
} }
} }

3
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ro-RO.json

@ -142,6 +142,7 @@
"MinimumSearchContent": "Trebuie să introduceţi cel putin 3 caractere!", "MinimumSearchContent": "Trebuie să introduceţi cel putin 3 caractere!",
"Volo.AbpIo.Domain:060001": "Sursa URL(\"{ArticleUrl}\") nu este URL GitHub", "Volo.AbpIo.Domain:060001": "Sursa URL(\"{ArticleUrl}\") nu este URL GitHub",
"Volo.AbpIo.Domain:060002": "Conţinutul articolului nu este disponibil din resursa de pe GitHub(\"{ArticleUrl}\").", "Volo.AbpIo.Domain:060002": "Conţinutul articolului nu este disponibil din resursa de pe GitHub(\"{ArticleUrl}\").",
"Volo.AbpIo.Domain:060003": "Nu a fost găsit conţinutul articolului!" "Volo.AbpIo.Domain:060003": "Nu a fost găsit conţinutul articolului!",
"SeeMore": "Vezi mai mult"
} }
} }

3
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sk.json

@ -142,6 +142,7 @@
"MinimumSearchContent": "Musíte zadať aspoň 3 znaky!", "MinimumSearchContent": "Musíte zadať aspoň 3 znaky!",
"Volo.AbpIo.Domain:060001": "Zdrojová URL(\"{ArticleUrl}\") nie je URL Githubu", "Volo.AbpIo.Domain:060001": "Zdrojová URL(\"{ArticleUrl}\") nie je URL Githubu",
"Volo.AbpIo.Domain:060002": "Obsah článku nie je dostupný v Github zdroji(\"{ArticleUrl}\").", "Volo.AbpIo.Domain:060002": "Obsah článku nie je dostupný v Github zdroji(\"{ArticleUrl}\").",
"Volo.AbpIo.Domain:060003": "Nenašiel sa žiadny obsah článku!" "Volo.AbpIo.Domain:060003": "Nenašiel sa žiadny obsah článku!",
"SeeMore": "Vidět Víc"
} }
} }

3
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json

@ -142,6 +142,7 @@
"MinimumSearchContent": "您必须输入至少 3 个字符!", "MinimumSearchContent": "您必须输入至少 3 个字符!",
"Volo.AbpIo.Domain:060001": "源 URL(\"{ArticleUrl}\") 不是 Github URL", "Volo.AbpIo.Domain:060001": "源 URL(\"{ArticleUrl}\") 不是 Github URL",
"Volo.AbpIo.Domain:060002": "文章内容无法从 Github(\"{ArticleUrl}\") 资源中获得。", "Volo.AbpIo.Domain:060002": "文章内容无法从 Github(\"{ArticleUrl}\") 资源中获得。",
"Volo.AbpIo.Domain:060003": "没有找到文章内容!" "Volo.AbpIo.Domain:060003": "没有找到文章内容!",
"SeeMore": "查看更多"
} }
} }

2
common.props

@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<Version>5.0.1</Version> <Version>5.1.0</Version>
<NoWarn>$(NoWarn);CS1591;CS0436</NoWarn> <NoWarn>$(NoWarn);CS1591;CS0436</NoWarn>
<PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl> <PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl>
<PackageProjectUrl>https://abp.io/</PackageProjectUrl> <PackageProjectUrl>https://abp.io/</PackageProjectUrl>

18
docs/en/Blog-Posts/2021-11-18 v5_0_Preview/POST.md

@ -127,13 +127,7 @@ First of all, you need to have EF Core or MongoDB installed into your solution.
#### Install the packages #### Install the packages
Install the new [Volo.Abp.EventBus.Boxes](https://www.nuget.org/packages/Volo.Abp.EventBus.Boxes) NuGet package to your database layer (to `EntityFrameworkCore` or `MongoDB` project) or to the host application. Open a command-line terminal at the root directory of your database (or host) project and execute the following command: For the outbox & inbox functionality, ABP depends on [DistributedLock.Core](https://www.nuget.org/packages/DistributedLock.Core) library which provides a distributed locking system for concurrency control in a distributed environment. There are [many distributed lock providers](https://github.com/madelson/DistributedLock#implementations), including Redis, SqlServer and ZooKeeper. You can use the one you like. Here, I will show the Redis provider.
````csharp
abp add-package Volo.Abp.EventBus.Boxes
````
This will install the package and setup the ABP module dependency. This package depends on [DistributedLock.Core](https://www.nuget.org/packages/DistributedLock.Core) library which provides a distributed locking system for concurrency control in a distributed environment. There are [many distributed lock providers](https://github.com/madelson/DistributedLock#implementations), including Redis, SqlServer and ZooKeeper. You can use the one you like. Here, I will show the Redis provider.
Add [DistributedLock.Redis](https://www.nuget.org/packages/DistributedLock.Redis) NuGet package to your project, then add the following code into the ConfigureService method of your ABP module class: Add [DistributedLock.Redis](https://www.nuget.org/packages/DistributedLock.Redis) NuGet package to your project, then add the following code into the ConfigureService method of your ABP module class:
@ -285,6 +279,16 @@ This can be a breaking change in rare cases (for example, if you create host sid
## Community News ## Community News
### ABP Community Talks 2021.12
![community-talks](community-talks.png)
As the core ABP development team, we've decided to organize monthly live meetings with the ABP community. The first live meeting will be at **December 16, 2021, 17:00 (UTC)** on YouTube. ABP core team members will present some of the new features coming with ABP 5.0.
**Join this event on the Kommunity platform: https://kommunity.com/volosoft/events/abp-community-talks-4afca9c9**
You can also [subscribe to the Volosoft channel](https://www.youtube.com/channel/UCO3XKlpvq8CA5MQNVS6b3dQ) for reminders for further ABP events and videos.
### ABP was on ASP.NET Community Startup! ### ABP was on ASP.NET Community Startup!
It was great for us to be invited to Microsoft's [ASP.NET Community Weekly Standup](https://dotnet.microsoft.com/live/community-standup) show, at September 28. There was a very high attention and that made us very happy. Thanks to the ABP Community and all the watchers :) If you've missed the talk, [you can watch it here](https://www.youtube.com/watch?v=vMWM-_ihjwM). It was great for us to be invited to Microsoft's [ASP.NET Community Weekly Standup](https://dotnet.microsoft.com/live/community-standup) show, at September 28. There was a very high attention and that made us very happy. Thanks to the ABP Community and all the watchers :) If you've missed the talk, [you can watch it here](https://www.youtube.com/watch?v=vMWM-_ihjwM).

BIN
docs/en/Blog-Posts/2021-11-18 v5_0_Preview/community-talks.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 KiB

BIN
docs/en/Blog-Posts/2021-11-18 v5_0_Preview/cover-50.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

47
docs/en/Blog-Posts/2021-12-15 v5_0_Release_Stable/POST.md

@ -0,0 +1,47 @@
# ABP.IO Platform 5.0 Final Has Been Released!
[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 5.0 versions have been released today.
## What's new with 5.0?
Since all the new features are already explained in details with the [5.0 RC.1 Announcement Post](https://blog.abp.io/abp/ABP-IO-Platform-5.0-RC-1-Has-Been-Released), I will not repeat all the details again. See the [RC Blog Post](https://blog.abp.io/abp/ABP-IO-Platform-5.0-RC-1-Has-Been-Released) for all the features and enhancements.
## Getting started with 5.0
### Creating new solutions
You can create a new solution with the ABP Framework version 5.0 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started).
Type the following command in a command-line terminal to install the ABP CLI version 5.0:
````bash
dotnet tool install -g Volo.Abp.Cli --version 5.0.0
````
To upgrade your existing ABP CLI installation:
````bash
dotnet tool update -g Volo.Abp.Cli --version 5.0.0
````
Then you can create a new solution using the `abp new` command:
````bash
abp new Acme.BookStore
````
> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for details.
### Upgrading existing solutions
Check [the migration guide](https://docs.abp.io/en/abp/latest/Migration-Guides/Abp-5_0) for the applications with the version 4.x upgrading to the version 5.0. Also see [the upgrading guide](https://docs.abp.io/en/abp/latest/Upgrading) to understand how to update existing solutions.
## ABP Community Talks 2021.12
![community-talks](community-talks.png)
As the core ABP development team, we've decided to organize monthly live meetings with the ABP community. The first live meeting will be at **December 16, 2021, 17:00 (UTC)** on YouTube. ABP core team members will present some of the new features coming with ABP 5.0.
**Join this event on the Kommunity platform: https://kommunity.com/volosoft/events/abp-community-talks-4afca9c9**
See you in the event!

BIN
docs/en/Blog-Posts/2021-12-15 v5_0_Release_Stable/community-talks.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 KiB

12
docs/en/CLI.md

@ -29,6 +29,7 @@ Here, the list of all available commands before explaining their details:
* **`help`**: Shows help on the usage of the ABP CLI. * **`help`**: Shows help on the usage of the ABP CLI.
* **`new`**: Generates a new solution based on the ABP [startup templates](Startup-Templates/Index.md). * **`new`**: Generates a new solution based on the ABP [startup templates](Startup-Templates/Index.md).
* **`update`**: Automatically updates all ABP related NuGet and NPM packages in a solution. * **`update`**: Automatically updates all ABP related NuGet and NPM packages in a solution.
* **`clean`**: Deletes all `BIN` and `OBJ` folders in the current folder.
* **`add-package`**: Adds an ABP package to a project. * **`add-package`**: Adds an ABP package to a project.
* **`add-module`**: Adds a [multi-package application module](https://docs.abp.io/en/abp/latest/Modules/Index) to a solution. * **`add-module`**: Adds a [multi-package application module](https://docs.abp.io/en/abp/latest/Modules/Index) to a solution.
* **`list-modules`**: Lists names of open-source application modules. * **`list-modules`**: Lists names of open-source application modules.
@ -145,6 +146,17 @@ abp update [options]
* `--check-all`: Check the new version of each package separately. Default is `false`. * `--check-all`: Check the new version of each package separately. Default is `false`.
* `--version` or `-v`: Specifies the version to use for update. If not specified, latest version is used. * `--version` or `-v`: Specifies the version to use for update. If not specified, latest version is used.
### clean
Deletes all `BIN` and `OBJ` folders in the current folder.
Usage:
````bash
abp clean
````
### add-package ### add-package
Adds an ABP package to a project by, Adds an ABP package to a project by,

17
docs/en/Community-Articles/2021-06-17-Using-Elsa-Workflow-with-ABP-Framework/POST.md

@ -241,8 +241,9 @@ private void ConfigureElsa(ServiceConfigurationContext context, IConfiguration c
.WithExposedHeaders("Content-Disposition")) .WithExposedHeaders("Content-Disposition"))
); );
//register controllers inside elsa //Uncomment the below line if your abp version is lower than v4.4 to register controllers of Elsa .
context.Services.AddAssemblyOf<Elsa.Server.Api.Endpoints.WorkflowRegistry.Get>(); //See https://github.com/abpframework/abp/pull/9299 (we will no longer need to specify this line of code from v4.4)
// context.Services.AddAssemblyOf<Elsa.Server.Api.Endpoints.WorkflowRegistry.Get>();
//Disable antiforgery validation for elsa //Disable antiforgery validation for elsa
Configure<AbpAntiForgeryOptions>(options => Configure<AbpAntiForgeryOptions>(options =>
@ -269,11 +270,7 @@ public override void OnApplicationInitialization(ApplicationInitializationContex
} }
``` ```
* In here we've specified the Elsa Server Api's assembly by using the `AddAssemblyOf<>` extension method to register the required services (controllers). These services required for the dashboard (if we create a workflow by using **Elsa Workflow Designer** it calls some services under the hook, therefore we need to be assured about these services get registered). * These services required for the dashboard.
* With [v4.4](https://github.com/abpframework/abp/pull/9299), we will no longer need to specify this line of code.
> **Note:** `AddAssemblyOf<>` extension method can help you to register all your services by convention. You can check [here](https://docs.abp.io/en/abp/latest/Dependency-Injection#conventional-registration) for more information about conventional registration.
* We don't need to register our workflows one by one anymore. Because now we use `.AddWorkflowsFrom<Startup>()`, and this registers workflows on our behalf. * We don't need to register our workflows one by one anymore. Because now we use `.AddWorkflowsFrom<Startup>()`, and this registers workflows on our behalf.
@ -371,12 +368,14 @@ namespace ElsaDemo.Permissions
<link rel="icon" type="image/png" sizes="32x32" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/images/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/images/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/images/favicon-16x16.png">
<link rel="stylesheet" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/fonts/inter/inter.css"> <link rel="stylesheet" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/fonts/inter/inter.css">
<link rel="stylesheet" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/styles/tailwind.css"> <link rel="stylesheet" href="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/elsa-workflows-studio.css">
<script src="/_content/Elsa.Designer.Components.Web/monaco-editor/min/vs/loader.js"></script> <script src="/_content/Elsa.Designer.Components.Web/monaco-editor/min/vs/loader.js"></script>
<script type="module" src="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/elsa-workflows-studio.esm.js"></script> <script type="module" src="/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/elsa-workflows-studio.esm.js"></script>
</head> </head>
<body class="h-screen" style="background-size: 30px 30px; background-image: url(/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/images/tile.png); background-color: #FBFBFB;"> <body class="h-screen" style="background-size: 30px 30px; background-image: url(/_content/Elsa.Designer.Components.Web/elsa-workflows-studio/assets/images/tile.png); background-color: #FBFBFB;">
<elsa-studio-root server-url="@serverUrl" monaco-lib-path="_content/Elsa.Designer.Components.Web/monaco-editor/min"></elsa-studio-root> <elsa-studio-root server-url="@serverUrl" monaco-lib-path="_content/Elsa.Designer.Components.Web/monaco-editor/min">
<elsa-studio-dashboard></elsa-studio-dashboard>
</elsa-studio-root>
</body> </body>
</html> </html>
``` ```

1671
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/POST.md

File diff suppressed because it is too large

BIN
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/application-contracts-folder-structure.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/application-final-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

BIN
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/application-folder-structure.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/book-create.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

BIN
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/book-creation-modal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/book-update-modal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/demo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/domain-file-structure.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/domain-shared-file-structure.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/er-diagram.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/web-folder-structure.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

321
docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/POST.md

@ -0,0 +1,321 @@
# Integrating the Syncfusion MVC Components to the ABP MVC UI
## Introduction
In this article we will see how we can integrate the Syncfusion MVC Components into our ABP application.
## Source Code
You can find the source code of the application at https://github.com/EngincanV/ABP-Syncfusion-Components-Demo.
## Prerequisites
* [.NET 6](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
* In this article, we will create a new startup template in v5.0.0-rc.2 and if you follow this article from top to bottom and create a new startup template with me, you need to install the [.NET 6 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) before starting.
**NOTE:** ABP v5.X stable version has been released. You can replace v5.0.0-rc.2 with the latest stable version in your steps.
Also, you need to update your ABP CLI to the v5.0.0-rc.2, you can use the command below to update your CLI version:
```bash
dotnet tool update Volo.Abp.Cli -g --version 5.0.0-rc.2
```
or install it if you haven't installed it before:
```bash
dotnet tool install Volo.Abp.Cli -g --version 5.0.0-rc.2
```
## Creating the Solution
In this article, we will create a new startup template with EF Core as a database provider and MVC for the UI framework. But if you already have a project with MVC UI, you don't need to create a new startup template, you can directly implement the following steps to your existing project.
> If you already have a project with MVC/Razor Pages UI, you can skip this section.
We can create a new startup template by using the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI):
```bash
abp new SyncfusionComponentsDemo -t app --preview
```
Our project boilerplate will be ready after the download is finished. Then, we can open the solution and start developing.
## Starting the Development
### Pre-requisite
> If you've already had a license from Syncfusion, you can skip this section.
* The first thing we need to do is create an account to be able to get a license from Syncfusion.
* So, let's navigate to https://www.syncfusion.com/aspnet-core-ui-controls and click the "Download Free Trial" button.
* Then fill the form and start your 30-day free trial.
* After that, navigate to https://www.syncfusion.com/account/manage-trials/downloads to get our license key that will be used in our application.
![](./manage-trial-1.png)
Click the "Get License Key" link for "ASP.NET Core (Essential JS 2)".
![](./manage-trial-2.png)
Then a modal will be opened like in the above image, select a version and click the "Get License Key" button.
![](./copy-license-key.png)
Lastly, copy the generated license key value.
In order to use the relevant components, Syncfusion needs to check this license key to know that our license is valid.
### Configurations
After providing a license key from Syncfusion, we can start with the configuration that needs to be done in our application.
#### 1-) Install the Syncfusion.EJ2.AspNet.Core package
We need to install the `Syncfusion.EJ2.AspNet.Core` Nuget package to our Web project (*.Web).
We can install it via **Visual Studio's Nuget Package Manager**:
![](./syncfusion-package.png)
or via dotnet cli:
```bash
dotnet add package Syncfusion.EJ2.AspNet.Core --version 19.3.0.57
```
> In this article, I've used the package in version 19.3.0.57.
#### 2-) Register the License Key
* After installing the package, we need to register our license key to be able to use the Syncfusion Components.
* To register the license key, open your web module class and update the `ConfigureServices` method as follows:
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
var hostingEnvironment = context.Services.GetHostingEnvironment();
var configuration = context.Services.GetConfiguration();
//Register Syncfusion license
Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense(licenseKey: configuration["Syncfusion:LicenseKey"].ToString());
ConfigureUrls(configuration);
ConfigureBundles();
ConfigureAuthentication(context, configuration);
ConfigureAutoMapper();
ConfigureVirtualFileSystem(hostingEnvironment);
ConfigureLocalizationServices();
ConfigureNavigationServices();
ConfigureAutoApiControllers();
ConfigureSwaggerServices(context.Services);
}
```
Instead of writing the license key directly in here we can define it in the **appsettings.json** file and use it here by using the Configuration system of .NET.
* Open your **appsettings.json** file and add a new section named "Syncfusion" as below:
```json
{
//...
"Syncfusion": {
"LicenseKey": "<your-license-key>"
}
}
```
> Replace the `<your-license-key> part with your license key that we've obtained in the previous section.`
* To be able to use the Syncfusion Components we need to define them in our **_ViewImports.cshtml** file. By doing that we can use the Syncfusion components everywhere in our application.
* Open your **/Pages/_ViewImports.cshtml** file and add a new tag helper:
```cshtml
@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
@addTagHelper *, Syncfusion.EJ2 //use Syncfusion components
```
#### 3-) Adding Syncfusion styles and scripts to our application
Firstly, let's install the `@syncfusion/ej2` package from **npm**.
* Open your **package.json** file and add the `@syncfusion/ej2` package with version **19.3.57**:
```json
{
"version": "1.0.0",
"name": "my-app",
"private": true,
"dependencies": {
"@abp/aspnetcore.mvc.ui.theme.basic": "^5.0.0-rc.2",
"@syncfusion/ej2": "^19.3.57"
}
}
```
* Then, open the **abp.resourcemapping.js** file and update the **mappings** section:
```js
module.exports = {
aliases: {
},
mappings: {
"@node_modules/@syncfusion/ej2/dist/ej2.min.js": "@libs/syncfusion/",
"@node_modules/@syncfusion/ej2/material.css": "@libs/syncfusion/"
}
};
```
> ABP copies related packages from **node_modules** folder to the **libs** folder by examining this file. You can read this [document](docs.abp.io/en/abp/latest/UI/AspNetCore/Client-Side-Package-Management#mapping-the-library-resources) for more info.
* Then run the `abp install-libs` to install the dependencies and copy them into the libs folder by your mappings configuration. After running this command, in your **libs** folder it should be a folder named **syncfusion** folder.
![](./syncfusion-libs.png)
The last thing we need to do is, add some style and script files provided by Syncfusion, between our head-body tags.
* We can do this by creating two view components (one for Styles and the other for Scripts). Let's do that.
First, create a folder structure as shown below under the **Components** folder.
![](./component-folder-structure.png)
Then open the related files and add the following codes to each of these files.
* **Default.cshtml** (/Components/Syncfusion/Script/Default.cshtml)
```cshtml
@addTagHelper *, Syncfusion.EJ2 //add this line
<!-- Syncfusion Essential JS 2 Scripts -->
<script src="/libs/syncfusion/ej2.min.js"></script>
<!-- Syncfusion Essential JS 2 ScriptManager -->
<ejs-scripts></ejs-scripts>
```
* **SyncfusionScriptComponent.cs**
```csharp
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;
namespace SyncfusionComponentsDemo.Web.Components.Syncfusion.Script
{
public class SyncfusionScriptComponent : AbpViewComponent
{
public IViewComponentResult Invoke()
{
return View("~/Components/Syncfusion/Script/Default.cshtml");
}
}
}
```
* **Default.cshtml** (/Components/Syncfusion/Style/Default.cshtml)
```cshtml
<!-- Syncfusion Essential JS 2 Styles -->
<link rel="stylesheet" href="/libs/syncfusion/material.css">
```
* SyncfusionStyleComponent.cs
```csharp
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;
namespace SyncfusionComponentsDemo.Web.Components.Syncfusion.Style
{
public class SyncfusionStyleComponent : AbpViewComponent
{
public IViewComponentResult Invoke()
{
return View("~/Components/Syncfusion/Style/Default.cshtml");
}
}
}
```
After creating these two components, we can use the [**Layout Hooks**](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Layout-Hooks) system of ABP to inject these two components between head and script tags.
To do this, open your web module class and update the `ConfigureServices` method as below:
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
var hostingEnvironment = context.Services.GetHostingEnvironment();
var configuration = context.Services.GetConfiguration();
//Register Syncfusion license
var licenseKey = configuration["Syncfusion:LicenseKey"].ToString();
Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense(licenseKey: licenseKey);
Configure<AbpLayoutHookOptions>(options =>
{
//Now, the SyncfusionStyleComponent code will be inserted in the head of the page as the last item.
options.Add(LayoutHooks.Head.Last, typeof(SyncfusionStyleComponent));
//the SyncfusionScriptComponent will be inserted in the body of the page as the last item.
options.Add(LayoutHooks.Body.Last, typeof(SyncfusionScriptComponent));
});
ConfigureUrls(configuration);
ConfigureBundles();
ConfigureAuthentication(context, configuration);
ConfigureAutoMapper();
ConfigureVirtualFileSystem(hostingEnvironment);
ConfigureLocalizationServices();
ConfigureNavigationServices();
ConfigureAutoApiControllers();
ConfigureSwaggerServices(context.Services);
}
```
After injecting the Syncfusion style and script into our application, our configurations have been completed. We can try with a simple component to see if it works as we expected.
* Let's try with the [Calendar](https://www.syncfusion.com/aspnet-core-ui-controls/calendar) component. Open your **Index.cshtml** file and update with the below content:
```cshtml
@page
@using Microsoft.AspNetCore.Mvc.Localization
@using SyncfusionComponentsDemo.Localization
@using Volo.Abp.Users
@model SyncfusionComponentsDemo.Web.Pages.IndexModel
@section styles {
<abp-style src="/Pages/Index.css" />
}
@section scripts {
<abp-script src="/Pages/Index.js" />
}
<div class="container">
<h2>Syncfusion - Calendar Component</h2>
<ejs-calendar id="calendar"></ejs-calendar>
</div>
```
* Then when we run the application, we need to see the relevant calendar component as below.
![](./calendar-component.png)
### Conclusion
In this article, we've explained how to integrate the **Syncfusion Components** into our applications. After following this article, you can use the Syncfusion components in your application.
Thanks for reading the article, I hope you've found it useful :)

BIN
docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/calendar-component.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/component-folder-structure.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/copy-license-key.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/manage-trial-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/manage-trial-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/syncfusion-libs.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/en/Community-Articles/2021-12-13-Integrating-the-Syncfusion-MVC-Components-to-the-ABP-MVC-UI/syncfusion-package.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

2
docs/en/Customizing-Application-Modules-Extending-Entities.md

@ -63,8 +63,6 @@ You can then use the same extra properties system defined in the previous sectio
Another approach can be **creating your own entity** mapped to **the same database table** (or collection for a MongoDB database). Another approach can be **creating your own entity** mapped to **the same database table** (or collection for a MongoDB database).
`AppUser` entity in the [application startup template](Startup-Templates/Application.md) already implements this approach. [EF Core Migrations document](Entity-Framework-Core-Migrations.md) describes how to implement it and manage **EF Core database migrations** in such a case. It is also possible for MongoDB, while this time you won't deal with the database migration problems.
## Creating a New Entity with Its Own Database Table/Collection ## Creating a New Entity with Its Own Database Table/Collection
Mapping your entity to an **existing table** of a depended module has a few disadvantages; Mapping your entity to an **existing table** of a depended module has a few disadvantages;

2
docs/en/Getting-Started-Running-Solution.md

@ -139,7 +139,7 @@ Ensure that the `.IdentityServer` project is the startup project. Run the applic
> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. > Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster.
You can login, but you cannot enter to the main application here. This is just the authentication server. You can login, but you cannot enter to the main application here. This is **just the authentication server**.
Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI:

2
docs/en/Getting-Started.md

@ -15,6 +15,6 @@
This tutorial explains how to **create and run** a new web application using the ABP Framework. Follow the steps below; This tutorial explains how to **create and run** a new web application using the ABP Framework. Follow the steps below;
1. [Setup your development environment](Getting-Started-Setup-Environment) 1. [Setup your development environment](Getting-Started-Setup-Environment.md)
2. [Creating a new solution](Getting-Started-Create-Solution.md) 2. [Creating a new solution](Getting-Started-Create-Solution.md)
3. [Running the solution](Getting-Started-Running-Solution.md) 3. [Running the solution](Getting-Started-Running-Solution.md)

2
docs/en/Tutorials/Part-2.md

@ -654,7 +654,7 @@ Open the `Books.razor` and replace the content as the following:
> If you see some syntax errors, you can ignore them if your application properly built and run. Visual Studio still has some bugs with Blazor. > If you see some syntax errors, you can ignore them if your application properly built and run. Visual Studio still has some bugs with Blazor.
* Inherited from the `AbpCrudPageBase<IBookAppService, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>` which implements all the CRUD details for us. * Inherited from the `AbpCrudPageBase<IBookAppService, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>` which implements all the CRUD details for us.
* `Entities`, `TotalCount`, `PageSize`, `OnDataGridReadAsync` are defined in the base blass. * `Entities`, `TotalCount`, `PageSize`, `OnDataGridReadAsync` are defined in the base class.
* Injected `IStringLocalizer<BookStoreResource>` (as `L` object) and used for localization. * Injected `IStringLocalizer<BookStoreResource>` (as `L` object) and used for localization.
While the code above pretty easy to understand, you can check the Blazorise [Card](https://blazorise.com/docs/components/card/) and [DataGrid](https://blazorise.com/docs/extensions/datagrid/) documents to understand them better. While the code above pretty easy to understand, you can check the Blazorise [Card](https://blazorise.com/docs/components/card/) and [DataGrid](https://blazorise.com/docs/extensions/datagrid/) documents to understand them better.

2
docs/en/Tutorials/Part-5.md

@ -142,7 +142,7 @@ Now, you can use the permissions to authorize the book management.
### Application Layer & HTTP API ### Application Layer & HTTP API
Open the `BookAppService` class and add set the policy names as the permission names defined above: Open the `BookAppService` class and set the policy names as the permission names defined above:
````csharp ````csharp
using System; using System;

2
docs/en/Virtual-File-System.md

@ -119,7 +119,7 @@ The Virtual File System is well integrated to ASP.NET Core:
### Static Virtual File Folders ### Static Virtual File Folders
By default, ASP.NET Core only allows the `wwwroot` folder to contain the static files consumed by the clients. When you use the birtual File System, the following folders also can contain static files: By default, ASP.NET Core only allows the `wwwroot` folder to contain the static files consumed by the clients. When you use the virtual File System, the following folders also can contain static files:
* Pages * Pages
* Views * Views

1
docs/zh-Hans/Background-Workers.md

@ -2,7 +2,6 @@
## 介绍 ## 介绍
背景工人在应用简单独立的线程在后台运行.一般来说,他们定期运行,以执行一些任务.例子;
后台工作者在应用程序后台运行的简单的独立线程,一般来说它们定期运行执行一些任务.例如; 后台工作者在应用程序后台运行的简单的独立线程,一般来说它们定期运行执行一些任务.例如;
* 后台工作者可以定期**删除过时的日志**. * 后台工作者可以定期**删除过时的日志**.

11
docs/zh-Hans/CLI.md

@ -29,6 +29,7 @@ dotnet tool update -g Volo.Abp.Cli
* **`help`**: 展示ABP CLI的用法帮助信息. * **`help`**: 展示ABP CLI的用法帮助信息.
* **`new`**:生成基于ABP的[启动模板](Startup-Templates/Index.md). * **`new`**:生成基于ABP的[启动模板](Startup-Templates/Index.md).
* **`update`**:自动更新的ABP解决方案ABP相关的NuGet和NPM包. * **`update`**:自动更新的ABP解决方案ABP相关的NuGet和NPM包.
* **`clean`**: 删除当前目录下所有的 `BIN``OBJ` 子目录.
* **`add-package`**: 添加ABP包到项目. * **`add-package`**: 添加ABP包到项目.
* **`add-module`**: 添加[应用模块](https://docs.abp.io/en/abp/latest/Modules/Index)到解决方案. * **`add-module`**: 添加[应用模块](https://docs.abp.io/en/abp/latest/Modules/Index)到解决方案.
* **`generate-proxy`**: 生成客户端代理以使用HTTP API端点. * **`generate-proxy`**: 生成客户端代理以使用HTTP API端点.
@ -129,6 +130,16 @@ abp update [options]
* `--check-all`: 分别检查每个包的新版本. 默认是 `false`. * `--check-all`: 分别检查每个包的新版本. 默认是 `false`.
* `--version` or `-v`: 指定用于升级的版本. 如果没有指定,则使用最新版本. * `--version` or `-v`: 指定用于升级的版本. 如果没有指定,则使用最新版本.
### clean
删除当前目录下所有的 `BIN``OBJ` 子目录.
用法:
````bash
abp clean
````
### add-package ### add-package
通过以下方式将ABP包添加到项目中 通过以下方式将ABP包添加到项目中

94
docs/zh-Hans/Getting-Started-AspNetCore-Application.md

@ -1,12 +1,12 @@
# 在AspNet Core MVC Web Application中使用ABP # 在ASP.NET Core MVC Web 应用程序中使用ABP
本教程将介绍如何开始以最少的依赖关系开始使用ABP开发. 本教程将介绍如何开始以最少的依赖关系开始使用ABP开发.
通常情况下你需要下载一个 **[启动模板](Getting-Started-AspNetCore-MVC-Template.md)** 通常情况下你希望从 **[启动模板](Getting-Started-AspNetCore-MVC-Template.md)** 开始.
## 创建一个新项目 ## 创建一个新项目
1. 使用Visual Studio 2019 (16.4.0+)创建一个新的AspNet Core Web Application: 1. 使用Visual Studio 2022 (17.0.0+)创建一个新的ASP.NET Core Web应用程序:
![](images/create-new-aspnet-core-application-v2.png) ![](images/create-new-aspnet-core-application-v2.png)
@ -21,7 +21,7 @@
## 安装 Volo.Abp.AspNetCore.Mvc 包 ## 安装 Volo.Abp.AspNetCore.Mvc 包
Volo.Abp.AspNetCore.Mvc是ABP集成AspNet Core MVC的包,请安装它到你项目中: Volo.Abp.AspNetCore.Mvc是ABP集成ASP.NET Core MVC的包,请安装它到你项目中:
```` ````
Install-Package Volo.Abp.AspNetCore.Mvc Install-Package Volo.Abp.AspNetCore.Mvc
@ -29,7 +29,7 @@ Install-Package Volo.Abp.AspNetCore.Mvc
## 创建ABP模块 ## 创建ABP模块
ABP是一个模块化框架,它需要一个**启动 (根) 模块**继承自``AbpModule``: ABP是一个模块化框架,它需要一个**启动(根)模块**继承自 `AbpModule`:
````C# ````C#
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -43,21 +43,20 @@ namespace BasicAspNetCoreApplication
[DependsOn(typeof(AbpAspNetCoreMvcModule))] [DependsOn(typeof(AbpAspNetCoreMvcModule))]
public class AppModule : AbpModule public class AppModule : AbpModule
{ {
public override void OnApplicationInitialization( public override void OnApplicationInitialization(ApplicationInitializationContext context)
ApplicationInitializationContext context)
{ {
var app = context.GetApplicationBuilder(); var app = context.GetApplicationBuilder();
var env = context.GetEnvironment(); var env = context.GetEnvironment();
// Configure the HTTP request pipeline.
if (env.IsDevelopment()) if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{ {
app.UseExceptionHandler("/Error"); app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
} }
app.UseHttpsRedirection();
app.UseStaticFiles(); app.UseStaticFiles();
app.UseRouting(); app.UseRouting();
app.UseConfiguredEndpoints(); app.UseConfiguredEndpoints();
@ -68,49 +67,44 @@ namespace BasicAspNetCoreApplication
``AppModule`` 是应用程序启动模块的好名称. ``AppModule`` 是应用程序启动模块的好名称.
ABP的包定义了这个模块类,模块可以依赖其它模块.在上面的代码中 ``AppModule`` 依赖于 ``AbpAspNetCoreMvcModule`` (模块存在于[Volo.Abp.AspNetCore.Mvc](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc)包中). 安装新的ABP的包后添加``DependsOn``是很常见的做法. ABP的包定义了模块类,模块可以依赖其它模块.在上面的代码中 ``AppModule`` 依赖于 ``AbpAspNetCoreMvcModule`` (由[Volo.Abp.AspNetCore.Mvc](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc)包定义). 安装新的ABP的包后添加``DependsOn``特性是很常见的做法.
我们在此模块类中配置ASP.NET Core管道,而不是Startup类中. 我们在此模块类中配置ASP.NET Core管道,而不是Startup类中.
### 启动类 ### 启动类
接下来修改启动类集成ABP模块系统: 接下来修改启动类集成ABP模块系统:
````C# ````C#
using Microsoft.AspNetCore.Builder; using BasicAspNetCoreApplication;
using Microsoft.Extensions.DependencyInjection;
namespace BasicAspNetCoreApplication var builder = WebApplication.CreateBuilder(args);
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddApplication<AppModule>();
}
public void Configure(IApplicationBuilder app) builder.Services.ReplaceConfiguration(builder.Configuration);
{
app.InitializeApplication(); builder.Services.AddApplication<AppModule>();
}
} var app = builder.Build();
}
app.InitializeApplication();
app.Run();
```` ````
``services.AddApplication<AppModule>()``添加了所有``AppModule``模块中定义的全部服务. ``services.AddApplication<AppModule>()``添加了从``AppModule``中启动的所有模块中定义的所有服务.
``Configure``方法中的``app.InitializeApplication()``完成初始化并启动应用程序. ``app.InitializeApplication()`` 初始化并启动应用程序.
## 运行应用程序! ## 运行应用程序!
启动该应用它将按预期运行. 启动该应用,它将按预期运行.
## 使用 Autofac 依赖注入框架 ## 使用 Autofac 依赖注入框架
虽然AspNet Core的依赖注入(DI)系统适用于基本要求,但[Autofac](https://autofac.org/)提供了属性注入和方法拦截等高级功能,这些功能是ABP执行高级应用程序框架功能所必需的. 虽然ASP.NET Core的依赖注入(DI)系统适用于基本要求,但[Autofac](https://autofac.org/)提供了属性注入和方法拦截等高级功能,这些功能是ABP执行高级应用程序框架功能所必需的.
用Autofac取代AspNet Core的DI系统并集成到ABP非常简单. 用Autofac取代ASP.NET Core的DI系统并集成到ABP非常简单.
1. 安装 [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) 包 1. 安装 [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) 包
@ -132,27 +126,21 @@ public class AppModule : AbpModule
3. 修改``Program.cs``以使用Autofac: 3. 修改``Program.cs``以使用Autofac:
````C# ````C#
using Microsoft.AspNetCore.Hosting; using BasicAspNetCoreApplication;
using Microsoft.Extensions.Hosting;
namespace BasicAspNetCoreApplication var builder = WebApplication.CreateBuilder(args);
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) => builder.Host.UseAutofac(); //Add this line
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => builder.Services.ReplaceConfiguration(builder.Configuration);
{
webBuilder.UseStartup<Startup>(); builder.Services.AddApplication<AppModule>();
})
.UseAutofac(); // 添加这一行 var app = builder.Build();
}
} app.InitializeApplication();
app.Run();
```` ````
## 源码 ## 源码

73
docs/zh-Hans/Getting-Started-Create-Solution.md

@ -0,0 +1,73 @@
# 入门教程
````json
//[doc-params]
{
"UI": ["MVC", "Blazor", "BlazorServer", "NG"],
"DB": ["EF", "Mongo"],
"Tiered": ["Yes", "No"]
}
````
> 本文档假设你更喜欢使用 **{{ UI_Value }}** 作为 UI 框架, 使用 **{{ DB_Value }}** 作为数据库提供程序. 对于其他选项, 请更改本文档顶部的首选项.
## 创建新项目
我们将使用 ABP CLI 创建一个新的 ABP 项目.
> 或者, 你可以使用[ABP Framework 网站](https://abp.io/get-started)页面上的选项轻松的 **创建并下载** 项目.
使用 ABP CLI 的 `new` 命令创建一个新项目:
````shell
abp new Acme.BookStore{{if UI == "NG"}} -u angular{{else if UI == "Blazor"}} -u blazor{{else if UI == "BlazorServer"}} -u blazor-server{{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes"}}{{if UI == "MVC" || UI == "BlazorServer"}} --tiered{{else}} --separate-identity-server{{end}}{{end}}
````
*你可以使用不同级别的命名空间, 例如: BookStore、Acme.BookStore或 Acme.Retail.BookStore.*
{{ if Tiered == "Yes" }}
{{ if UI == "MVC" || UI == "BlazorServer" }}
* `--tified` 参数用于创建认证服务器、 UI 和 API 实际分隔的 N-层解决方案.
{{ else }}
* `--separate-identity-server` 参数用于将Identity Server应用程序与API主机应用程序分隔开. 如果未指定, 则服务器上将只有一个端点.
{{ end }}
{{ end }}
> [ABP CLI 文档](./CLI.md) 涵盖了所有可用的命令和选项.
## 移动端开发
如果你想要在你的解决方案中包含 [React Native](https://reactnative.dev/) 项目, 将 `-m react-native` (or `--mobile react-native`) 参数添加到项目创建命令. 这是一个基础的 React Native 启动模板, 用于开发基于你的 ABP 后端的移动应用程序.
请参阅 [React Native 入门](Getting-Started-React-Native.md) 文档, 了解如何配置和运行 React Native 应用程序.
### 解决方案结构
该解决方案具有分层结构 (基于 [域驱动设计](Domain-Driven-Design.md)), 并包含单元 & 集成测试项目. 请参阅 [应用程序模板文档](Startup-Templates/Application.md) 以详细了解解决方案结构.
{{ if DB == "Mongo" }}
#### MongoDB 事务
[启动模板](Startup-templates/Index.md) 默认在`.MongoDB`项目中**禁用**事务. 如果你的MongoDB服务器支持事务, 你可以在*YourProjectMongoDbModule*类中的`ConfigureServices`方法开启它:
```csharp
Configure<AbpUnitOfWorkDefaultOptions>(options =>
{
options.TransactionBehavior = UnitOfWorkTransactionBehavior.Auto;
});
```
> 或者你可以删除该代码, 因为 `Auto` 已经是默认行为.
{{ end }}
## 下一步
* [运行解决方案](Getting-Started-Running-Solution.md)

199
docs/zh-Hans/Getting-Started-Running-Solution.md

@ -0,0 +1,199 @@
# 入门教程
````json
//[doc-params]
{
"UI": ["MVC", "Blazor", "BlazorServer", "NG"],
"DB": ["EF", "Mongo"],
"Tiered": ["Yes", "No"]
}
````
> 本文档假设你更喜欢使用 **{{ UI_Value }}** 作为 UI 框架, 使用 **{{ DB_Value }}** 作为数据库提供程序. 对于其他选项, 请更改本文档顶部的首选项.
## 创建数据库
### 连接字符串
检查在 {{if Tiered == "Yes"}}`.IdentityServer` 和`.HttpApi.Host` 项目{{else}}{{if UI=="MVC"}}`.Web` 项目{{else if UI=="BlazorServer"}}`.Blazor` 项目{{else}}`.HttpApi.Host` 项目{{end}}{{end}} 中 `appsettings.json` 文件里的**连接字符串**.
{{ if DB == "EF" }}
````json
"ConnectionStrings": {
"Default": "Server=(LocalDb)\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True"
}
````
> **关于连接字符串和数据库管理系统**
>
> 解决方案配置为默认使用 **Entity Framework Core****MS SQL Server**. 但是, 如果在执行ABP CLI 的`new`命令时使用了`-dbms`参数来选择其他DBMS (如`-dbms MySQL`), 那么连接字符串可能不同.
>
> EF Core 支持 [多种](https://docs.microsoft.com/en-us/ef/core/providers/) 据库提供程序, 因此你可以使用任何受支持的DBMS. 你可以需要时候参阅[Entity Framework 集成文档](Entity-Framework-Core.md) 来学习如何[切换到另一个DBMS](Entity-Framework-Core-Other-DBMS.md).
### 数据库迁移
该解决方案使用[Entity Framework Core Code First 迁移](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). 带有 `.DbMigrator` 的控制台程序用于 **应用迁移****初始化种子数据**. 它在**开发**和**生产**环境中都很有用.
> `.dbMigator` 项目有自己的 `appsettings.json`. 因此, 如果你更改了之前的连接字符串, 那么也应该更改这个连接字符串.
### 初次迁移
`.dbMigator` 应用程序在首次运行时自动**创建初始迁移**.
**如果你使用的是 Visual Studio, 你可以跳到 *运行 dbMigrator* 部分.** 但是, 其他 IDE (例如 Rider) 在首次运行时可能会遇到问题, 因为它会添加初始迁移并编译项目. 在这种情况下, 请在 `.dbMigration` 项目的文件夹中打开命令行终端, 然后运行以下命令:
````bash
dotnet run
````
下次, 你可以像往常一样在 IDE 中运行它.
### 运行迁移
右键单击 `.dbMigration` 项目, 然后选择 **设置为启动项目**
![set-as-startup-project](images/set-as-startup-project.png)
按F5(或Ctrl + F5) 运行应用程序. 它将具有如下所示的输出:
![db-migrator-output](images/db-migrator-output.png)
> 初始的[种子数据](Data-Seeding.md)在数据库中创建了 `admin` 用户(密码为`1q2w3E*`) 用于登录应用程序. 所以, 对于新数据库至少使用 `.DbMigrator` 一次.
{{ else if DB == "Mongo" }}
````json
"ConnectionStrings": {
"Default": "mongodb://localhost:27017/BookStore"
}
````
该解决方案配置为在本地计算机中使用 **MongoDB**, 因此你需要启动并运行 MongoDB 服务器实例, 或者将连接字符串更改为另一台 MongoDB 服务器.
### 种子初始数据
该解决方案带有 `.DbMigrator` 的控制台程序用于 **初始化种子数据**. 它在**开发**和**生产**环境中都很有用.
> `.dbMigator` 项目有自己的 `appsettings.json`. 因此, 如果你更改了之前的连接字符串, 那么也应该更改这个连接字符串.
右键单击 `.dbMigration` 项目, 然后选择 **设置为启动项目**
![set-as-startup-project](images/set-as-startup-project.png)
按F5(或Ctrl + F5) 运行应用程序. 它将具有如下所示的输出:
![db-migrator-output](images/db-migrator-output.png)
> 初始的[种子数据](Data-Seeding.md)在数据库中创建了 `admin` 用户(密码为`1q2w3E*`) 用于登录应用程序. 所以, 对于新数据库至少使用 `.DbMigrator` 一次.
{{ end }}
## 运行应用程序
{{ if UI == "MVC" || UI == "BlazorServer" }}
{{ if Tiered == "Yes" }}
> 分层解决方案使用 **Redis** 作为分布式缓存. 确保它已安装并在本地计算机上运行. 如果你使用的是远程 Redis 服务器, 请修改项目的 ` appsettings.json` 文件中的配置.
1. 确保 `.IdentityServer` 项目是启动项目. 运行此应用程序, 它将在浏览器中打开 **登录** 页面.
> 在 Visual Studio 中使用 Ctrl+F5(而不是F5) 在不进行调试的情况下运行应用程序. 如果你没有调试目的, 这会更快.
你可以登录, 但不能在这里进入主应用程序. 这 **只是身份验证服务器**.
2. 确保 `.httpapi.Host` 项目是启动项目, 然后运行应用将在浏览器中打开 **Swagger UI**.
![swagger-ui](images/swagger-ui.png)
这是 Web 应用程序使用的 HTTP API.
3. 最后, 确保 {{if UI=="MVC"}}`.Web`{{else}}`.Blazor`{{end}} 项目是启动项目,然后运行应用程序,它将在浏览器中打开 **欢迎** 页面
![mvc-tiered-app-home](images/bookstore-home.png)
单击 **login** 按钮, 它将重定向到 *身份验证服务器* 以登录到应用程序:
![bookstore-login](images/bookstore-login.png)
{{ else # Tiered != "Yes" }}
确保 {{if UI=="MVC"}}`.Web`{{else}}`.Blazor`{{end}} 项目是启动项目. 运行应用程序将会在浏览器中打开 **login** 页面:
> 在 Visual Studio 中使用 Ctrl+F5(而不是F5) 在不进行调试的情况下运行应用程序. 如果你没有调试目的, 这会更快.
![bookstore-login](images/bookstore-login.png)
{{ end # Tiered }}
{{ else # UI != MVC || BlazorServer }}
### 运行 HTTP API 主机(服务器端)
{{ if Tiered == "Yes" }}
> 分层解决方案使用 Redis 作为分布式缓存. 确保它已安装并在本地计算机上运行. 如果你使用的是远程 Redis 服务器, 请修改项目的 `appsettings.json` 文件中的配置.
确保 `.IdentityServer` 项目是启动项目. 运行此应用程序, 它将在浏览器中打开 **登录** 页面.
> 在 Visual Studio 中使用 Ctrl+F5(而不是F5) 在不进行调试的情况下运行应用程序. 如果你没有调试目的, 这会更快.
你可以登录, 但不能在这里进入主应用程序. 这 **只是身份验证服务器**.
确保 `.HttpApi.Host` 项目是启动项目, 然后运行应用程序将打开 Swagger UI 的:
{{ else # Tiered == "No" }}
确保 `.HttpApi.Host` 项目是启动项目, 然后运行应用程序将打开 Swagger UI 的:
> 在 Visual Studio 中使用 Ctrl+F5(而不是F5) 在不进行调试的情况下运行应用程序. 如果你没有调试目的, 这会更快.
{{ end # Tiered }}
![swagger-ui](images/swagger-ui.png)
你可以在这里查看应用程序的API并测试它们. 获取Swagger UI的[更多信息](https://swagger.io/tools/swagger-ui/).
{{ end # UI }}
{{ if UI == "Blazor" }}
### 运行 Blazor 应用程序 (客户端)
确保 `.Blazor` 项目是启动项目并运行应用程序.
> 在 Visual Studio 中使用 Ctrl+F5(而不是F5) 在不进行调试的情况下运行应用程序. 如果你没有调试目的, 这会更快.
应用程序启动后, 单击页头上的 **Login** 链接, 你将重定向到身份验证服务器以输入用户名和密码:
![bookstore-login](images/bookstore-login.png)
{{ else if UI == "NG" }}
### 运行 Angular 应用程序 (客户端)
转到 `Angular` 文件夹, 打开命令行终端, 键入 `yarn` 命令(我们建议使用 [yarn](https://yarnpkg.com/) 软件包管理器, 而 `npm install` 也可以使用)
```bash
yarn
```
当所有node模块加载完毕后, 执行 `yarn start` (或 `npm start`) 命令:
```bash
yarn start
```
初次构建可能需要更长的时间. 完成后, 它会在默认浏览器中使用 [localhost:4200](http://localhost:4200/) 地址打开 Angular UI.
![bookstore-login](images/bookstore-login.png)
{{ end }}
输入用户名 **admin** 和 密码 **1q2w3E*** 登录应用程序. 应用程序已启动并正在运行. 你可以根据此启动模板开始开发应用程序.
## 另请参见
* [Web 应用程序开发教程](Tutorials/Part-1.md)
* [应用程序启动模板](Startup-Templates/Application.md)

53
docs/zh-Hans/Getting-Started-Setup-Environment.md

@ -0,0 +1,53 @@
# 入门教程
````json
//[doc-params]
{
"UI": ["MVC", "Blazor", "BlazorServer", "NG"],
"DB": ["EF", "Mongo"],
"Tiered": ["Yes", "No"]
}
````
> 本文档假设你更喜欢使用 **{{ UI_Value }}** 作为 UI 框架, 使用 **{{ DB_Value }}** 作为数据库提供程序. 对于其他选项, 请更改本文档顶部的首选项.
## 设置你的开发环境
第一件事! 在创建项目之前, 让我们先设置你的开发环境.
### 先决条件
开发计算机上应安装以下工具:
* 一个集成开发环境 (比如: [Visual Studio](https://visualstudio.microsoft.com/vs/)) 它需要支持 [.NET 6.0+](https://dotnet.microsoft.com/download/dotnet) 的开发.
{{ if UI != "Blazor" }}
* [Node v12 或 v14](https://nodejs.org/)
* [Yarn v1.20+ (不是v2)](https://classic.yarnpkg.com/en/docs/install) <sup id="a-yarn">[1](#f-yarn)</sup> 或 npm v6+ (已跟随Node一起安装)
{{ end }}
{{ if Tiered == "Yes" }}
* [Redis](https://redis.io/) (启动解决方案使用 Redis 作为 [分布式缓存](Caching.md)).
{{ end }}
{{ if UI != "Blazor" }}
<sup id="f-yarn"><b>1</b></sup> _Yarn v2 工作方式不同, 不被支持._ <sup>[↩](#a-yarn)</sup>
{{ end }}
### 安装 ABP CLI
[ABP CLI](./CLI.md) 是一个命令行界面, 用于自动执行基于 ABP 的解决方案的一些常见任务. 首先, 你需要使用以下命令安装 ABP CLI:
````shell
dotnet tool install -g Volo.Abp.Cli
````
如果已安装, 则可以使用以下命令对其进行更新:
````shell
dotnet tool update -g Volo.Abp.Cli
````
## 下一步
* [创建新的解决方案](Getting-Started-Create-Solution.md)

370
docs/zh-Hans/Getting-Started.md

@ -1,374 +1,20 @@
## 入门 # 入门教程
````json ````json
//[doc-params] //[doc-params]
{ {
"UI": ["MVC","NG"], "UI": ["MVC", "Blazor", "BlazorServer", "NG"],
"DB": ["EF", "Mongo"], "DB": ["EF", "Mongo"],
"Tiered": ["Yes", "No"] "Tiered": ["Yes", "No"]
} }
```` ````
本教程介绍了如何创建一个新的{{if UI == "MVC"}} ASP.NET Core MVC web {{else if UI == "NG"}} Angular {{end}}. 配置并运行它. > 本文档假设你更喜欢使用 **{{ UI_Value }}** 作为 UI 框架, 使用 **{{ DB_Value }}** 作为数据库提供程序. 对于其他选项, 请更改本文档顶部的首选项.
## 设置你的开发环境 ## 内容
创建第一个项目之前,需要正确的设置你的开发环境. 本教程介绍如何使用 ABP 框架 **创建和运行** 新的 Web 应用程序. 请按照以下步骤操作;
### 预先要求 1. [设置你的开发环境](Getting-Started-Setup-Environment.md)
2. [创建新的解决方案](Getting-Started-Create-Solution.md)
你需要安装以下工具: 3. [运行解决方案](Getting-Started-Running-Solution.md)
* [Visual Studio 2019 (v16.4+)](https://visualstudio.microsoft.com/vs/) for Windows / [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).<sup id="a-editor">[1](#f-editor)</sup>
* [.NET Core 3.1+](https://www.microsoft.com/net/download/dotnet-core/)
* [Node v12 或 v14](https://nodejs.org/en/)
* [Yarn v1.19+](https://classic.yarnpkg.com/)
* [Yarn v1.20+ (not v2)](https://classic.yarnpkg.com/en/docs/install) <sup id="a-yarn">[2](#f-yarn)</sup> 或 npm v6+ (与Node一起安装)
{{ if Tiered == "Yes" }}
* [Redis](https://redis.io/): 入门解决方案将Redis用作[分布式缓存](Caching.md). 因此你需要安装并运行Redis.
{{ end }}
<sup id="f-editor"><b>1</b></sup> _只要支持.NET Core和ASP.NET Core,就可以使用其他编辑器代替Visual Studio._ <sup>[↩](#a-editor)</sup>
<sup id="f-yarn"><b>2</b></sup> _Yarn v2 的工作方式不同,不受支持._ <sup>[↩](#a-yarn)</sup>
### 安装ABP CLI
[ABP CLI](./CLI.md)是一个命令行页面,用于自动执行一些基于ABP的应用程序的常见任务.
> ABP CLI是ABP框架一个免费开源的工具.
你需要使用以下命令安排ABP CLI:
````shell
dotnet tool install -g Volo.Abp.Cli
````
如果你已经安装,你可以使用以下命令更新到最新版本:
````shell
dotnet tool update -g Volo.Abp.Cli
````
## 创建新项目
> 本文假设你使用 **{{ UI_Value }}** 做为UI框架 **{{ DB_Value }}** 做为数据库提供程序,对于其它选项,你可以更改文档顶部的首选项.
### 使用ABP CLI创建一个新项目
使用ABP CLI的 `new` 命令创建新项目:
````shell
abp new Acme.BookStore{{if UI == "NG"}} -u angular {{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes" && UI != "NG"}} --tiered {{else if Tiered == "Yes" && UI == "NG"}}--separate-identity-server{{end}} --mobile react-native
````
* 此命令还会在解决方案文件夹内创建一个React Native移动应用程序. 如果你不想要它,可以安全地删除它或从`abp new`命令中删除`--mobile react-native`选项, 以使其完全不包含在解决方案中.
{{ if UI == "NG" }}
* `-u` 指定UI框架, 本例中是 `angular`.
{{ if Tiered == "Yes" }}
* `--separate-identity-server` 参数用于将Identity服务器应用程序与API主机应用程序分隔开. 如果未指定,则服务器上将只有一个端点.
{{ end }}
{{ end }}
{{ if DB == "Mongo" }}
* `-d` 指定数据库提供程序, 本例中是 `mongodb`.
{{ end }}
{{ if Tiered == "Yes" && UI != "NG" }}
* `--tiered` 参数用于创建n层解决方案,其中身份验证服务器层,UI层和API层在物理上是分离的.
{{ end }}
> 你可以使用不同级别的命令空间; 例如. BookStore, Acme.BookStore or Acme.Retail.BookStore.
#### ABP CLI 命令 & 选项
[ABP CLI文档](./CLI.md)涵盖了ABP CLI的所有可用命令和选项. 本文档使用[应用程序启动模板](Startup-Templates/Application.md)创建新的Web应用程序. 有关其他模板,请参见[ABP启动模板](Startup-Templates/Index.md)文档.
> 或者,您可以从[ABP Framework网站](https://abp.io/get-started)中选择"直接下载"选项卡创建新的解决方案.
## 解决方案结构
{{ if UI == "MVC" }}
创建项目后你会有以下解决方案目录和文件:
![](images/solution-files-mvc.png)
在Visual Studio中打开 `.sln` 文件时,将看到以下解决方案结构:
{{if DB == "Mongo"}}
![vs-default-app-solution-structure](images/vs-app-solution-structure-mongodb.png)
{{else}}
![vs-default-app-solution-structure](images/vs-app-solution-structure{{if Tiered == "Yes"}}-tiered{{end}}.png)
{{end}}
{{ else if UI == "NG" }}
在创建的解决方案中有三个文件夹:
![](images/solution-files-non-mvc.png)
* `angular` 文件夹包含Angular UI应用程序.
* `aspnet-core` 文件夹包含后端应用程序.
* `react-native` 文件夹包含React Native UI 应用程序.
打开 `aspnet-core` 文件夹下的 `.sln`(`Visual Studio`解决方案)文件:
![vs-angular-app-backend-solution-structure](images/vs-spa-app-backend-structure{{if DB == "Mongo"}}-mongodb{{end}}.png)
{{ end }}
> ###### 关于解决方案中的项目
>
> 根据你的**UI**,**数据库**和其他选项,你的解决方案的结构可能略有不同.
该解决方案具有分层结构(基于[Domain Driven Design](Domain-Driven-Design.md)), 并包含配置好的的单元&集成测试项目.
{{ if DB == "EF" }}
集成测试项目已配置为可与 **EF Core** & **SQLite 内存** database同时使用.
{{ else if DB == "Mongo" }}
集成测试项目已配置为每个测试创建的内存中的**MongoDB**数据库(使用的[Mongo2Go](https://github.com/Mongo2Go/Mongo2Go)库).
{{ end }}
> 请参阅[应用程序模板文档](Startup-Templates/Application.md)详细了解解决方案结构.
{{ if DB == "Mongo" }}
> [启动模板](Startup-templates/Index.md)默认在 `.MongoDB` 项目中**禁用**了工作单元事务. 如果你的MongoDB服务器支持事务,你可以手动启用工作单元的事务:
```csharp
Configure<AbpUnitOfWorkDefaultOptions>(options =>
{
options.TransactionBehavior = UnitOfWorkTransactionBehavior.Enabled;
});
```
{{ end }}
## 创建数据库
### 连接字符串
检查 {{if UI == "MVC"}}{{if Tiered == "Yes"}}`.IdentityServer` 和 `.HttpApi.Host` 项目{{else}}`.Web` 项目{{end}}{{else if UI == "NG" }}`.HttpApi.Host` 项目{{end}}下 `appsettings.json` 文件中的 **链接字符串**:
{{ if DB == "EF" }}
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True"
}
````
该解决方案配置为**Entity Framework Core**与**MS SQL Server**一起使用. EF Core支持[各种](https://docs.microsoft.com/en-us/ef/core/providers/)数据库提供程序,因此你可以使用任何受支持的DBMS. 请参阅[Entity Framework集成文档](https://docs.abp.io/en/abp/latest/Entity-Framework-Core)了解如何切换到另一个DBMS.
### 数据库连接字符串
查看`.Web`项目下`appsettings.json`文件中的 **连接字符串**:
````json
{
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True"
}
}
````
解决方案使用 **Entity Framework Core****MS SQL Server**. EF Core支持[各种](https://docs.microsoft.com/zh-cn/ef/core/providers/)数据库提供程序,因此你可以根据实际需要使用其他DBMS. 如果需要,请更改连接字符串.
### 应用迁移
该解决方案使用[Entity Framework Core Code First 迁移](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). 你需要应用迁移来创建数据库,有两种方法迁移数据库.
#### 使用DbMigrator应用程序应用迁移
该解决方案包含一个控制台应用程序(在此示例中名为`Acme.BookStore.DbMigrator`),可以创建数据库,应用迁移和初始化数据. 它对开发和生产环境都很有用.
> `.DbMigrator`项目有自己的`appsettings.json`. 因此,如果你更改了上面的连接字符串,则还应更改此字符串.
右键单击`.DbMigrator`项目并选择 **设置为启动项目**:
![set-as-startup-project](images/set-as-startup-project.png)
按F5(或Ctrl + F5)运行应用程序. 它将具有如下所示的输出:
![set-as-startup-project](images/db-migrator-app.png)
#### 使用EF Core Update-Database命令
Ef Core具有`Update-Database`命令, 可根据需要创建数据库并应用挂起的迁移. 右键单击`.Web`项目并选择**设置为启动项目**:
{{ if UI == "MVC" }}
右键单击{{if Tiered == "Yes"}}`.IdentityServer`{{else}}`.Web`{{end}}项目并选择**设置为启动项目**:
{{ else if UI != "MVC" }}
右键单击`.HttpApi.Host`项目并选择**设置为启动项目**:
{{ end }}
![set-as-startup-project](images/set-as-startup-project.png)
打开**包管理器控制台(Package Manager Console)**, 选择`.EntityFrameworkCore.DbMigrations`项目作为**默认项目**并运行`Update-Database`命令:
![package-manager-console-update-database](images/package-manager-console-update-database.png)
这将基于配置的连接字符串创建新数据库.
> **使用`.DbMigrator`工具是建议的方法**, 因为它能初始化初始数据能够正确运行Web应用程序.
>
> 如果你只是使用 `Update-Database` 命令,你会得到一个空数据库,所以你无法登录到应用程序因为数据库中没有初始管理用户. 不需要种子数据库时,可以在开发期间使用 `Update-Database` 命令. 但是使用 `.DbMigrator` 应用程序会更简单,你始终可以使用它来迁移模式并为数据库添加种子.
{{ else if DB == "Mongo" }}
````json
"ConnectionStrings": {
"Default": "mongodb://localhost:27017/BookStore"
}
````
该解决方案被配置为在你的本地计算机中使用 **MongoDB**,因此你需要启动并运行一个MongoDB服务器实例或者将连接字符串更改为另一个MongoDB服务器.
### 初始化种子数据
该解决方案附带一个 `.DbMigrator` 控制台应用程序,该应用程序为初始数据提供了种子. 它对于开发以及生产环境都很有用.
> `.DbMigrator` 项目有自己的 `appsettings.json`.如果你更改了其他项目的 `appsettings.json`,也应该更改这个.
右键点击 `.DbMigrator` 并选择 **设置为启动项目**.
![set-as-startup-project](images/set-as-startup-project.png)
按F5(或Ctrl+F5)启动应用程序,你会看到以下输出:
![db-migrator-output](images/db-migrator-output.png)
> 数据库创建后会初始化[种子数据](Data-Seeding.md), 其中包含用于登录的 `admin` 用户. 所以你至少使用 `.DbMigrator` 一次.
{{ end }}
### 运行应用程序
{{ if UI == "MVC" }}
{{ if Tiered == "Yes" }}
确保 `.IdentityServer` 是启动项目,运行应用程序后会在你的浏览器打开一个 **login** 页面.
> 在Visual Studio中使用Ctrl+F5(而不是F5)运行应用,如果你不用于调试,这会减少启动时间.
你可以登录,但是不能在这里进入主应用程序,它仅是验证服务器.
确保 `.HttpApi.Host` 是启动项目,运行应用程序后会在你的浏览器打开一个 **Swagger UI** 页面.
![swagger-ui](images/swagger-ui.png)
这里是Web应用程序使用的API应用程序.
最后确保 `.Web` 是启动项目,运行应用程序后会在你的浏览器打开一个 **welcome** 页面.
![mvc-tiered-app-home](images/bookstore-home.png)
点击 **login** 按钮重定向到 `Identity Server` 来登录应用程序.
![bookstore-login](images/bookstore-login.png)
{{ else }}
最后确保 `.Web` 是启动项目,运行应用程序后会在你的浏览器打开一个 **login** 页面.
> 在Visual Studio中使用Ctrl+F5(而不是F5)运行应用,如果你不用于调试,这会减少启动时间.
![bookstore-login](images/bookstore-login.png)
{{ end }}
{{ else if UI != "MVC" }}
#### 运行HTTP API Host (服务器端)
{{ if Tiered == "Yes" }}
确保 `.IdentityServer` 是启动项目,运行应用程序后会在你的浏览器打开一个 **login** 页面.
> 在Visual Studio中使用Ctrl+F5(而不是F5)运行应用,如果你不用于调试,这会减少启动时间.
你可以登录,但是不能在这里进入主应用程序,它仅是验证服务器.
{{ end }}
确保 `.HttpApi.Host` 是启动项目,运行应用程序后会在你的浏览器打开一个 **Swagger UI** 页面.
{{ if Tiered == "No" }}
> 在Visual Studio中使用Ctrl+F5(而不是F5)运行应用,如果你不用于调试,这会减少启动时间.
{{ end }}
![swagger-ui](images/swagger-ui.png)
你可以看到应用程序的API并进行测试. 更多信息,请参阅[Swagger UI](https://swagger.io/tools/swagger-ui/).
> ##### Swagger UI 授权
>
> 大多数的HTTP API都需要身份验证和授权. 如果你要测试授权API, 请手动进入 `/Account/Login` 页面, 输入用户名: `admin` 和密码: `1q2w3E*` 登录到应用程序. 然后你可以访问授权API.
{{ end }}
{{ if UI == "NG" }}
#### 运行 Angular 应用程序 (客户端)
`angular` 下打开命令行终端, 输入 `yarn` 命令(我们推荐使用[yarn](https://yarnpkg.com/)包管理, `npm install` 在大多数情况下也可以工作).
```bash
yarn
```
等到所有node模块加载成功, 执行 `yarn start` (或 `npm start`) 命令:
```bash
yarn start
```
等待 `Angular CLI` 使用 `BrowserSync` 启动 `Webpack` dev-server.
它会负责编译你的 `TypeScript`代码, 并自动重新加载浏览器.
完成后 `Angular Live Development Server` 会监听 localhost:4200.
打开你的浏览器并导航到[localhost:4200](http://localhost:4200/).
![bookstore-login](images/bookstore-login.png)
{{ end }}
输入用户名 **admin**,密码 **1q2w3E*** 登录到应用程序,应用程序已经启动并执行,你可以基于此启动模板开始开发应用程序.
#### 移动开发
当你创建一个新的应用程序时. 可以添加`-m react-native`选项以在解决方案中包含 `react-native`项目. 这是一个基础的[React Native](https://reactnative.dev/)启动模板,用于开发与基于ABP的后端集成的移动应用程序.
请参阅"[React Native入门](Getting-Started-React-Native.md)"文档了解如何配置和运行React Native应用程序.
## 下一步是什么?
[Web应用程序开发教程](Tutorials/Part-1.md)

310
docs/zh-Hans/Modules/Identity.md

@ -1,28 +1,320 @@
# 身份管理模块 # 身份管理模块
身份模块基于Microsoft Identity库用于管理[组织单元](Organization-Units.md), 角色, 用户和他们的权限. 身份模块基于 [Microsoft Identity 库](https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/identity) 用于管理角色,用户及其权限.
> 参阅 [源码](https://github.com/abpframework/abp/tree/dev/modules/identity). 文档很快会被完善. ## 如何安装
## Identity安全日志 当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时,此模块将被预安装(作为 NuGet/NPM 包)。你可以继续用其作为包并轻松地获取更新,也可以将其源代码包含在解决方案中(请参阅 `get-source` [CLI](../CLI.md))以开发自定义模块。
安全日志可以记录账户的一些重要的操作或者改动, 你可以在在一些功能中保存安全日志. ### 源代码
你可以注入和使用 `IdentitySecurityLogManager``ISecurityLogManager` 来保存安全日志. 默认它会创建一个安全日志对象并填充常用的值. 如 `CreationTime`, `ClientIpAddress`, `BrowserInfo`, `current user/tenant`等等. 当然你可以自定义这些值. 可以 [在此处](https://github.com/abpframework/abp/tree/dev/modules/identity) 访问源代码。源代码使用 [MIT](https://choosealicense.com/licenses/mit/) 许可, 所以你可以免费使用和自定义它.
## 用户界面
此模块提供了 [Blazor](../UI/Blazor/Overall.md), [Angular](../UI/Angular/Quick-Start.md) 和 [MVC / Razor Pages](../UI/AspNetCore/Overall.md) 的 UI 可选.
### 菜单项
此模块在 *管理* 菜单下添加了一个 *身份管理* 菜单项:
![身份模块菜单](../images/identity-module-menu.png)
这个菜单项和相关页面已获得授权. 这意味着当前用户必须拥有相关权限才能使其可见. `admin` 角色 (和拥有此角色的用户, 如 `admin` 用户) 已经拥有这些权限. 如果你想要使其他角色/用户也启用权限, 请打开 *角色**用户* 页面的 *权限* 对话框, 并检查如下所示的权限:
![身分模块权限](../images/identity-module-permissions.png)
请参阅 [授权文档](../Authorization.md) 以了解权限系统.
### 页面
本节介绍此模块提供的主要页面.
#### 用户
此页用于查看用户列表. 您可以创建/编辑和删除用户, 将角色分配给用户.
![身分模块用户](../images/identity-module-users.png)
一个用户可以有零个或多个角色. 用户从其角色继承权限. 此外, 你可以给用户直接分配权限 (通过点击 *操作* 按钮, 然后选择 *权限*) .
#### 角色
角色用于按分组给用户分配权限.
![身份模块角色](../images/identity-module-roles.png)
角色除了角色名称之外还有2个属性:
* `Default`: 如果某个角色被标记为 "default", 那么当新用户 (使用 [账户模块](Account.md)) 注册到应用程序时, 其将被作为默认角色分配给新用户.
* `Public`: 用户的公共角色可以被应用程序中的其他用户看到. 这个功能在身份模块中没有用到, 但被提供作为你可能希望在你自己的应用程序中使用的功能.
## 其他功能
本节包括此模块提供的没有 UI 页面的其他功能.
### 组织单元
组织单元 (OU) 可被用于 **按层级对用户和实体进行分组**.
#### 组织单元实体
组织单元由 **OrganizationUnit** 实体表示. 它的基本属性是:
- **TenantId**: 组织单元的租户Id. 对于宿主可以是 null.
- **ParentId**: 父级组织单元的Id. 如果这是一个根级组织单元,它可以是 null.
- **Code**: 对于租户唯一的层级字符串编码.
- **DisplayName**: 组织单元的显示名称.
#### 组织树
由于组织单元可以有父级, 因此租户的所有组织单元是一个 **树** 结构. 这个树有一些规则;
- 可以有多个根级 (`ParentId` 是 `null` 的) .
- 一个组织单元的第一级子项数量有限制 (因为下面解释的固定的组织单元编码单位长度) .
#### 组织单元编码
组织单元编码是通过 `OrganizationUnitManager` 服务自动生成和维护的. 它是一个字符串, 像这样:
"**00001.00042.00005**"
通常用这种编码可以轻易地 (递归) 查询出数据库中组织单元的所有子项. 这个编码有一些规则 (当你使用 `OrganizationUnitManager` 时自动被应用的) :
- 对 [租户](../Multi-Tenancy.md) 是 **唯一的**.
- 同一组织单元的全部子项都具有 **以父级组织单元编码开头的** 编码.
- 如示例中所示, 它是固定长度的且是基于树中组织单元的级别的.
- 尽管组织单元编码是唯一的, 但如果你移动了相关的组织单元, 它也可以被更改.
请注意, 你必须根据Id引用一个组织单元, 而不是编码, 因为编码后续是可以被更改的.
#### 组织单元管理
`OrganizationUnitManager` 类可以被 [注入](../Dependency-Injection.md) 并用来管理组织单元. 常见用例如下:
- 创建, 更新和删除组织单元.
- 在组织单元树中移动一个组织单元.
- 获取关于组织单元树及其项的信息.
### 身份安全日志
安全日志系统可以记录账户的一些重要的操作或者改动 (例如 *登录**更改密码*) . 如果需要, 你也可以保存安全日志.
你可以注入和使用 `IdentitySecurityLogManager``ISecurityLogManager` 来写入安全日志. 默认它会创建一个日志对象并填充常用的值, 如 `CreationTime`, `ClientIpAddress`, `BrowserInfo`, `current user/tenant` 等等. 当然你可以覆盖这些值.
```cs ```cs
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{ {
Identity = "IdentityServer"; Identity = "IdentityServer";
Action = "ChangePassword"; Action = "ChangePassword";
}); });
``` ```
通过配置 `AbpSecurityLogOptions` 来提供应用程序的名称或者禁用安全日志功能. 默认是**启用**状态. 通过配置 `AbpSecurityLogOptions`为日志提供应用程序的名称 (如果你有多个应用程序并且想要在日志中区分应用程序) 或者禁用安全日志功能.
```cs ```cs
Configure<AbpSecurityLogOptions>(options => Configure<AbpSecurityLogOptions>(options =>
{ {
options.ApplicationName = "AbpSecurityTest"; options.ApplicationName = "AbpSecurityTest";
}); });
``` ```
## 选项
`IdentityOptions` 是由 Microsoft [Identity 库](https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/identity) 提供的标准 [选项类](../Options.md) . 所以, 你可以在 [模块](../Module-Development-Basics.md) 类的 `ConfigureServices` 方法中设置这些选项.
**例如: 设置所需的密码最小长度**
````csharp
Configure<IdentityOptions>(options =>
{
options.Password.RequiredLength = 5;
});
````
ABP 允许你在运行时通过使用 [设置系统](../Settings.md) 更进一步地更改这些选项. 你可以 [注入](../Dependency-Injection.md) `ISettingManager` 并使用 `Set...` 方法中的一种来更改用户, 租户或全局所有用户的选项值.
**例如: 更改当前租户所需的密码最小长度**
````csharp
public class MyService : ITransientDependency
{
private readonly ISettingManager _settingManager;
public MyService(ISettingManager settingManager)
{
_settingManager = settingManager;
}
public async Task ChangeMinPasswordLength(int minLength)
{
await _settingManager.SetForCurrentTenantAsync(
IdentitySettingNames.Password.RequiredLength,
minLength.ToString()
);
}
}
````
`IdentitySettingNames` 类 (在 `Volo.Abp.Identity.Settings` 命名空间中) 为设置名称定义了一些常量.
## 分布式事件
此模块定义了如下 ETOs (事件传输对象) 以允许你去订阅模块中实体的改动.
* `UserEto``IdentityUser` 实体更改完成时发布.
* `IdentityRoleEto``IdentityRole` 实体更改完成时发布.
* `IdentityClaimTypeEto``IdentityClaimType` 实体更改完成时发布.
* `OrganizationUnitEto``OrganizationUnit` 实体更改完成时发布.
**例如: 当一个新用户被创建时接收通知**
````csharp
public class MyHandler :
IDistributedEventHandler<EntityCreatedEto<UserEto>>,
ITransientDependency
{
public async Task HandleEventAsync(EntityCreatedEto<UserEto> eventData)
{
UserEto user = eventData.Entity;
// TODO: ...
}
}
````
`UserEto``IdentityRoleEto` 默认自动发布事件. 你应该自己配置其他的 Eto. 请参阅 [分布式事件总线文档](../Distributed-Event-Bus.md) 了解预定义事件的详细信息.
> 订阅分布式事件对于分布式场景 (如微服务架构) 特别有用. 如果你正在构建单体式应用程序, 或者在运行了身份模块的同一进程内监听事件, 那么订阅 [本地事件](../Local-Event-Bus.md) 更高效, 更简单.
## 内部构件
本节包括模块的一些内部详细信息, 您不是那么需要, 但在一些情况下可能需要使用.
### 领域层
#### 聚合
##### 用户
用户通常是一个登录并使用应用程序的人.
* `IdentityUser` (聚合根): 表示系统中的一个用户.
* `IdentityUserRole` (集合): 分配给用户的角色.
* `IdentityUserClaim` (集合): 用户的自定义声明.
* `IdentityUserLogin` (集合): 用户的外部登录.
* `IdentityUserToken` (集合): 用户的令牌 (用于 Microsoft Identity 服务).
##### 角色
角色通常是分配给用户的一组权限.
* `IdentityRole` (聚合根): 表示系统中的角色.
* `IdentityRoleClaim` (集合): 角色的自定义声明.
##### 声明类型
声明类型是可以被分配给系统中其他实体 (如角色和用户) 的自定义声明的定义.
* `IdentityClaimType` (聚合根): 表示声明类型的定义. 它包括一些用于定义声明类型和验证规则的属性 (例如, 必须的, 正则表达式, 描述, 值类型) .
##### 身份安全日志
`IdentitySecurityLog` 对象表示系统中与授权相关的操作 (如 *登录*) .
* `IdentitySecurityLog` (聚合根): 表示系统中的安全日志.
##### 组织单元
组织单元是一个有层级结构的实体.
* ```OrganizationUnit``` (聚合根): 表示系统中的组织单元.
* ```Roles``` (集合): 组织单元的角色.
#### 仓储
此模块定义了以下自定义仓储:
* `IIdentityUserRepository`
* `IIdentityRoleRepository`
* `IIdentityClaimTypeRepository`
* ```IIdentitySecurityLogRepository```
* ```IOrganizationUnitRepository```
#### 领域服务
##### 用户管理
`IdentityUserManager` 常用于管理用户, 他们的权限, 声明, 密码, 电子邮件等等. 它派生自 Microsoft Identity 的 `UserManager<T>` 类, 其中 `T``IdentityUser`.
##### 角色管理
`IdentityRoleManager` 常用于管理角色和他们的声明. 它派生自 Microsoft Identity 的 `RoleManager<T>` 类, 其中 `T``IdentityRole`.
##### 声明类型管理
`IdenityClaimTypeManager` 常用于对聚合根 `IdentityClaimType` 执行某些操作.
##### 组织单元管理
```OrganizationUnitManager``` 常用于对聚合根 `OrganizationUnit` 执行某些操作.
##### 安全日志管理
```IdentitySecurityLogManager``` 常用于保存安全日志.
### 服务层
#### 应用服务
* `IdentityUserAppService` (实现 `IIdentityUserAppService`): 实现了用户管理 UI 的用例.
* `IdentityRoleAppService` (实现 `IIdentityRoleAppService`): 实现了角色管理 UI 的用例.
* `IdentityClaimTypeAppService` (实现 `IIdentityClaimTypeAppService`): 实现了声明类型管理 UI 的用例.
* `IdentitySettingsAppService` (实现 `IIdentitySettingsAppService`): 用于获取和更新身份模块的设置.
* `IdentityUserLookupAppService` (实现 `IIdentityUserLookupAppService`): 用于根据 `id``userName` 获取用户信息. 它旨在由ABP内部使用.
* `ProfileAppService` (实现 `IProfileAppService`): 用于更改用户的简介和密码.
* ```IdentitySecurityLogAppService``` (实现 ```IIdentitySecurityLogAppService```): 实现了安全日志 UI 的用例.
* ```OrganizationUnitAppService``` (实现 ```OrganizationUnitAppService```): 实现了组织单元管理 UI 的用例.
### 数据库提供程序
此模块为数据库提供 [Entity Framework Core](../Entity-Framework-Core.md) 和 [MongoDB](../MongoDB.md) 两种选择.
#### EF Core
NuGet 包 [Volo.Abp.Identity.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.Identity.EntityFrameworkCore) 实现了 EF Core 的集成.
##### 数据库表
* **AbpRoles**
* AbpRoleClaims
* **AbpUsers**
* AbpUserClaims
* AbpUserLogins
* AbpUserRoles
* AbpUserTokens
* **AbpClaimTypes**
* **AbpOrganizationUnits**
* AbpOrganizationUnitRoles
* AbpUserOrganizationUnits
* **AbpSecurityLogs**
#### MongoDB
NuGet 包 [Volo.Abp.Identity.MongoDB](https://www.nuget.org/packages/Volo.Abp.Identity.MongoDB) 实现了 MongoDB 的集成.
##### 数据库集合
* **AbpRoles**
* **AbpUsers**
* **AbpClaimTypes**
* **AbpOrganizationUnits**
* **AbpSecurityLogs**
#### 常用数据库属性
你可以设置 `AbpIdentityDbProperties` 中的以下属性来更改数据库选项:
* `DbTablePrefix` (`Abp` 作为默认值) 是表/集合名称的前缀.
* `DbSchema` (`null` 作为默认值) 是数据库架构.
* `ConnectionStringName` (`AbpIdentity` 作为默认值) 是此模块的 [连接字符串](../Connection-Strings.md) 名称.
它们是静态属性. 你需要在开始运行应用程序前设置它们 (通常在 `Program.cs` 中).

174
docs/zh-Hans/Modules/IdentityServer.md

@ -1 +1,173 @@
TODO... # 身份服务器模块
身份服务器模块提供了一个 [IdentityServer](https://github.com/IdentityServer/IdentityServer4) (IDS) 的完全集成, 该框架提供高级身份验证功能, 如单点登录和API访问控制.此模块将客户端,资源以及其他 IDS 相关的对象保存到数据库中.
## 如何安装
当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时, 此模块将被预安装(作为 NuGet/NPM 包).你可以继续用其作为包并轻松地获取更新, 也可以将其源代码包含在解决方案中(请参阅 `get-source` [CLI](../CLI.md))以开发自定义模块.
### 源代码
可以 [在此处](https://github.com/abpframework/abp/tree/dev/modules/identityserver) 访问源代码.源代码使用 [MIT](https://choosealicense.com/licenses/mit/) 许可, 所以你可以免费使用和自定义它.
## 用户界面
此模块使用了领域逻辑和数据库集成, 但没有提供任何 UI.如果你需要动态添加客户端和资源, 管理 UI 是非常有用的.在这种情况下, 你可以自己构建管理 UI, 或者考虑购买为此模块提供了管理 UI 的 [ABP 商业版](https://commercial.abp.io/).
## 与其他模块的关系
此模块基于 [身份模块](Identity.md) 并且[账户模块](Account.md) 有一个 [集成包](https://www.nuget.org/packages/Volo.Abp.Account.Web.IdentityServer).
## 选项
### AbpIdentityServerBuilderOptions
`AbpIdentityServerBuilderOptions` 在你的身份服务器 [模块](https://docs.abp.io/zh-Hans/abp/latest/Module-Development-Basics) 中的 `PreConfigureServices` 方法中配置.例如:
````csharp
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpIdentityServerBuilderOptions>(builder =>
{
//Set options here...
});
}
````
`AbpIdentityServerBuilderOptions` 属性:
* `UpdateJwtSecurityTokenHandlerDefaultInboundClaimTypeMap` (默认值:true):更新 `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap` 使其与身份服务器声明兼容.
* `UpdateAbpClaimTypes` (默认值:true):更新 `AbpClaimTypes` 与身份服务器声明兼容.
* `IntegrateToAspNetIdentity` (默认值:true):集成到 ASP.NET Identity.
* `AddDeveloperSigningCredential` (默认值:true):设置为 false 禁止调用 IIdentityServerBuilder 中的 `AddDeveloperSigningCredential()`.
`IIdentityServerBuilder` 可以在你的身份服务器 [模块](https://docs.abp.io/zh-Hans/abp/latest/Module-Development-Basics) 中的 `PreConfigureServices` 方法中配置.例如:
````csharp
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<IIdentityServerBuilder>(builder =>
{
builder.AddSigningCredential(...);
});
}
````
## 内部结构
### 领域层
#### 聚合
##### API 资源
需要 API 资源来允许客户端请求访问令牌.
* `ApiResource` (聚合根):表示系统中的 API 资源.
* `ApiSecret` (集合):API 资源的密钥.
* `ApiScope` (集合):API 资源的作用域.
* `ApiResourceClaim` (集合):API 资源的声明.
##### 客户端
客户端表示可以从你的身份服务器请求令牌的应用程序.
* `Client` (聚合根):表示一个身份服务器的客户端应用程序.
* `ClientScope` (集合):客户端的作用域.
* `ClientSecret` (集合):客户端的密钥.
* `ClientGrantType` (集合):客户端的授权类型.
* `ClientCorsOrigin` (集合):客户端的 CORS 源.
* `ClientRedirectUri` (集合):客户端的重定向 URIs.
* `ClientPostLogoutRedirectUri` (集合):客户端的登出重定向 URIs.
* `ClientIdPRestriction` (集合):客户端的提供程序约束.
* `ClientClaim` (集合):客户端的声明.
* `ClientProperty` (集合):客户端的自定义属性.
##### 持续化授权
持续化授权存储了授权码,刷新令牌和用户准许.
* `PersistedGrant` (聚合根):表示为身份服务器持续化授权.
##### 身份资源
身份资源是用户的用户 ID ,名称或邮件地址等数据.
* `IdentityResource` (聚合根):表示与身份服务器的身份资源.
* `IdentityClaim` (集合):身份资源的声明.
#### 仓储
为此模块定义了以下自定义仓储:
* `IApiResourceRepository`
* `IClientRepository`
* `IPersistentGrantRepository`
* `IIdentityResourceRepository`
#### 领域服务
此模块不包含任何领域服务, 但重写了下面的服务;
* `AbpProfileService` (当 `AbpIdentityServerBuilderOptions.IntegrateToAspNetIdentity` 为 true 时使用)
* `AbpClaimsService`
* `AbpCorsPolicyService`
### 设置
此模块未定义任何设置.
### 应用层
#### 应用服务
* `ApiResourceAppService` (实现 `IApiResourceAppService`):实现了 API 资源管理 UI 的用例.
* `IdentityServerClaimTypeAppService` (实现 `IIdentityServerClaimTypeAppService`):用于获取声明列表.
* `ApiResourceAppService` (实现 `IApiResourceAppService`):实现了 API 管理资源 UI 的用例.
* `IdentityResourceAppService` (实现 `IIdentityResourceAppService`):实现了身份资源管理 UI 的用例.
### 数据库提供程序
#### 公共
##### 表/集合 前缀 & 架构
所有表/集合都使用 `IdentityServer` 作为默认前缀.如果你需要改变表的前缀或设置一个架构名称(如果你的数据库提供程序支持), 请设置 `AbpIdentityServerDbProperties` 类的静态属性.
##### 连接字符串
此模块使用 `AbpIdentityServer` 作为连接字符串的名称.如果你没有用这个名称定义连接字符串, 它将回退到 `Default` 连接字符串.
有关详细信息, 请参阅 [连接字符串](https://docs.abp.io/zh-Hans/abp/latest/Connection-Strings) 文档.
#### EF Core
##### 表
* **IdentityServerApiResources**
* IdentityServerApiSecrets
* IdentityServerApiScopes
* IdentityServerApiScopeClaims
* IdentityServerApiClaims
* **IdentityServerClients**
* IdentityServerClientScopes
* IdentityServerClientSecrets
* IdentityServerClientGrantTypes
* IdentityServerClientCorsOrigins
* IdentityServerClientRedirectUris
* IdentityServerClientPostLogoutRedirectUris
* IdentityServerClientIdPRestrictions
* IdentityServerClientClaims
* IdentityServerClientProperties
* **IdentityServerPersistedGrants**
* **IdentityServerIdentityResources**
* IdentityServerIdentityClaims
#### MongoDB
##### 集合
* **IdentityServerApiResources**
* **IdentityServerClients**
* **IdentityServerPersistedGrants**
* **IdentityServerIdentityResources**

119
docs/zh-Hans/Tutorials/Part-1.md

@ -2,32 +2,15 @@
````json ````json
//[doc-params] //[doc-params]
{ {
"UI": ["MVC","NG"], "UI": ["MVC","Blazor","BlazorServer","NG"],
"DB": ["EF","Mongo"] "DB": ["EF","Mongo"]
} }
```` ````
{{
if UI == "MVC"
UI_Text="mvc"
else if UI == "NG"
UI_Text="angular"
else
UI_Text="?"
end
if DB == "EF"
DB_Text="Entity Framework Core"
else if DB == "Mongo"
DB_Text="MongoDB"
else
DB_Text="?"
end
}}
## 关于本教程 ## 关于本教程
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: 在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:
* **{{DB_Text}}** 做为ORM提供程序. * **{{DB_Text}}** 做为数据库提供程序.
* **{{UI_Value}}** 做为UI框架. * **{{UI_Value}}** 做为UI框架.
本教程分为以下部分: 本教程分为以下部分:
@ -40,16 +23,29 @@ end
- [Part 6: 作者: 领域层](Part-6.md) - [Part 6: 作者: 领域层](Part-6.md)
- [Part 7: 作者: 数据库集成](Part-7.md) - [Part 7: 作者: 数据库集成](Part-7.md)
- [Part 8: 作者: 应用服务层](Part-8.md) - [Part 8: 作者: 应用服务层](Part-8.md)
- [Part 9: 作者: 用户](Part-9.md) - [Part 9: 作者: 用户](Part-9.md)
- [Part 10: 图书到作者的关系](Part-10.md) - [Part 10: 图书到作者的关系](Part-10.md)
## 下载源码 ## 下载源码
本教程根据你的**UI** 和 **Database**偏好有多个版,我们准备了两种可供下载的源码组合: 本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合:
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) * [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) * [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
{{if UI == "MVC" && DB == "EF"}}
### 视频教程
本章也被录制为视频教程 **<a href="https://www.youtube.com/watch?v=cJzyIFfAlp8&list=PLsNclT2aHJcPNaCf7Io3DbMN6yAk_DgWJ&index=1" target="_blank">发布在YouTube</a>**.
{{end}}
## 创建解决方案 ## 创建解决方案
在开始开发之前,请按照[入门教程](../Getting-Started.md)创建名为 `Acme.BookStore` 的新解决方案. 在开始开发之前,请按照[入门教程](../Getting-Started.md)创建名为 `Acme.BookStore` 的新解决方案.
@ -58,12 +54,12 @@ end
启动模板中的**领域层**分为两个项目: 启动模板中的**领域层**分为两个项目:
- `Acme.BookStore.Domain`包含你的[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities), [领域服务](https://docs.abp.io/zh-Hans/abp/latest/Domain-Services)和其他核心域对象. - `Acme.BookStore.Domain`包含你的[实体](../Entities.md), [领域服务](../Domain-Services.md)和其他核心域对象.
- `Acme.BookStore.Domain.Shared`包含可与客户共享的常量,枚举或其他域相关对象. - `Acme.BookStore.Domain.Shared`包含可与客户共享的常量,枚举或其他域相关对象.
在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义你的实体. 在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义你的实体.
该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个 `Books` 文件夹并在其中添加一个名为 `Book` 的类,如下所示: 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个 `Books` 文件夹(命名空间),并在其中添加名为 `Book` 的类,如下所示:
````csharp ````csharp
using System; using System;
@ -84,15 +80,15 @@ namespace Acme.BookStore.Books
} }
```` ````
* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是[**领域驱动设计**](./Domain-Driven-Design.md) 概念之一. 可以视为直接查询和处理的根实体(请参阅[实体文档](../Entities.md)). * ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是[**领域驱动设计**](../Domain-Driven-Design.md) 概念之一. 可以视为直接查询和处理的根实体(请参阅[实体文档](../Entities.md)).
* `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些审计属性(`CreationTime`, `CreatorId`, `LastModificationTime` 等). ABP框架自动为你管理这些属性. * `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些基础[审计](../Audit-Logging.md)属性(例如`CreationTime`, `CreatorId`, `LastModificationTime` 等). ABP框架自动为你管理这些属性.
* `Guid`是`Book`实体的主键类型. * `Guid`是`Book`实体的**主键类型**.
> 为了保持简单,本教程将实体属性保留为 **public get/set** . 如果你想了解关于DDD最佳实践,请参阅[实体文档](../Entities.md). > 为了保持简单,本教程将实体属性保留为 **public get/set** . 如果你想了解关于DDD最佳实践,请参阅[实体文档](../Entities.md).
### BookType枚举 ### BookType枚举
上面所用到了 `BookType` 枚举,在 `Acme.BookStore.Domain.Shared` 项目创建 `BookType`. `Book`实体使用了`BookType`枚举. 在`Acme.BookStore.Domain.Shared`项目中创建`Books`文件夹(命名空间),并在其中添加`BookType`:
````csharp ````csharp
namespace Acme.BookStore.Books namespace Acme.BookStore.Books
@ -150,28 +146,37 @@ public class BookStoreMongoDbContext : AbpMongoDbContext
### 将Book实体映射到数据库表 ### 将Book实体映射到数据库表
`Acme.BookStore.EntityFrameworkCore` 项目中打开 `BookStoreDbContextModelCreatingExtensions.cs` 文件,添加 `Book` 实体的映射代码. 最终类应为:
打开`BookStoreDbContext`类的`OnModelCreating`方法,为`Book`实体添加映射代码:
````csharp ````csharp
using Acme.BookStore.Books; using Acme.BookStore.Books;
using Microsoft.EntityFrameworkCore; ...
using Volo.Abp;
using Volo.Abp.EntityFrameworkCore.Modeling;
namespace Acme.BookStore.EntityFrameworkCore namespace Acme.BookStore.EntityFrameworkCore
{ {
public static class BookStoreDbContextModelCreatingExtensions public class BookStoreDbContext :
AbpDbContext<BookStoreDbContext>,
IIdentityDbContext,
ITenantManagementDbContext
{ {
public static void ConfigureBookStore(this ModelBuilder builder) ...
protected override void OnModelCreating(ModelBuilder builder)
{ {
Check.NotNull(builder, nameof(builder)); base.OnModelCreating(builder);
/* Include modules to your migration db context */
builder.ConfigurePermissionManagement();
...
/* Configure your own tables/entities inside here */ /* Configure your own tables/entities inside here */
builder.Entity<Book>(b => builder.Entity<Book>(b =>
{ {
b.ToTable(BookStoreConsts.DbTablePrefix + "Books", b.ToTable(BookStoreConsts.DbTablePrefix + "Books",
BookStoreConsts.DbSchema); BookStoreConsts.DbSchema);
b.ConfigureByConvention(); //auto configure for the base class props b.ConfigureByConvention(); //auto configure for the base class props
b.Property(x => x.Name).IsRequired().HasMaxLength(128); b.Property(x => x.Name).IsRequired().HasMaxLength(128);
}); });
@ -180,14 +185,14 @@ namespace Acme.BookStore.EntityFrameworkCore
} }
```` ````
* `BookStoreConsts` 含有用于表的架构和表前缀的常量值. 你不必使用它,但建议在单点控制表前缀. * `BookStoreConsts` 含有用于表的架构和表前缀的常量值. 使用它不是强制的,但建议在统一的地方控制表前缀.
* `ConfigureByConvention()` 方法优雅的配置/映射继承的属性,应始终所有的实体使用它. * `ConfigureByConvention()` 方法优雅的配置/映射继承的属性,应对所有的实体使用它.
### 添加数据迁移 ### 添加数据迁移
启动模板使用[EF Core Code First Migrations](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/)创建和维护数据库架构. 我们应该创建一个新的迁移并且应用到数据库. 本示例使用[EF Core Code First Migrations](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/).因为我们修改了数据库映射配置,我们必须创建一个新的迁移并且应用到数据库.
`Acme.BookStore.EntityFrameworkCore.DbMigrations` 目录打开命令行终端输入以下命令: `Acme.BookStore.EntityFrameworkCore` 目录打开命令行终端输入以下命令:
```bash ```bash
dotnet ef migrations add Created_Book_Entity dotnet ef migrations add Created_Book_Entity
@ -205,7 +210,7 @@ dotnet ef migrations add Created_Book_Entity
> >在运行应用程序之前最好将初始数据添加到数据库中. 本节介绍ABP框架的[数据种子系统](../Data-Seeding.md). 如果你不想创建种子数据可以跳过本节,但是建议你遵循它来学习这个有用的ABP Framework功能。 > >在运行应用程序之前最好将初始数据添加到数据库中. 本节介绍ABP框架的[数据种子系统](../Data-Seeding.md). 如果你不想创建种子数据可以跳过本节,但是建议你遵循它来学习这个有用的ABP Framework功能。
`*.Domain` 项目下创建派生 `IDataSeedContributor` 的类,并且拷贝以下代码: `*.Domain` 项目下创建 `IDataSeedContributor`派生类,并且拷贝以下代码:
```csharp ```csharp
using System; using System;
@ -258,7 +263,7 @@ namespace Acme.BookStore
} }
``` ```
* 如果数据库中当前没有图书,则此代码使用 `IRepository<Book, Guid>`(默认[repository](../Repositories.md))将两本书插入数据库. * 如果数据库中当前没有图书,则此代码使用 `IRepository<Book, Guid>`(默认[repository](../Repositories.md))将两本书插入数据库.
### 更新数据库 ### 更新数据库
@ -279,7 +284,7 @@ namespace Acme.BookStore
### BookDto ### BookDto
`CrudAppService` 基类需要定义实体的基本DTO. 在 `Acme.BookStore.Application.Contracts` 项目中创建一个名为 `BookDto` 的DTO类: `CrudAppService` 基类需要定义实体的基本DTO. 在 `Acme.BookStore.Application.Contracts` 项目中创建 `Books` 文件夹(命名空间), 并在其中添加名为 `BookDto` 的DTO类:
````C# ````C#
using System; using System;
@ -300,9 +305,9 @@ namespace Acme.BookStore
} }
```` ````
* **DTO**类被用来在 **表示层****应用层** **传递数据**.查看[DTO文档](https://docs.abp.io/zh-Hans/abp/latest/Data-Transfer-Objects)查看更多信息. * **DTO**类被用来在 **表示层****应用层** **传递数据**.参阅[DTO文档](https://docs.abp.io/zh-Hans/abp/latest/Data-Transfer-Objects).
* 为了在面上展示书籍信息,`BookDto`被用来将书籍数据传递到表示层. * 为了在用户界面上展示书籍信息,`BookDto`被用来将书籍数据传递到表示层.
* `BookDto`继承自 `AuditedEntityDto<Guid>`.上面定义的 `Book` 实体一样具有一些审计属性. * `BookDto`继承自 `AuditedEntityDto<Guid>`.上面定义的 `Book` 实体一样具有一些审计属性.
在将书籍返回到表示层时,需要将`Book`实体转换为`BookDto`对象. [AutoMapper](https://automapper.org)库可以在定义了正确的映射时自动执行此转换. 启动模板配置了AutoMapper,因此你只需在`Acme.BookStore.Application`项目的`BookStoreApplicationAutoMapperProfile`类中定义映射: 在将书籍返回到表示层时,需要将`Book`实体转换为`BookDto`对象. [AutoMapper](https://automapper.org)库可以在定义了正确的映射时自动执行此转换. 启动模板配置了AutoMapper,因此你只需在`Acme.BookStore.Application`项目的`BookStoreApplicationAutoMapperProfile`类中定义映射:
@ -322,12 +327,11 @@ namespace Acme.BookStore
} }
```` ````
> 参阅 [对象对象映射](../Object-To-Object-Mapping.md) 文档了解详情. > 参阅 [对象对象映射](../Object-To-Object-Mapping.md) 文档了解详情.
### CreateUpdateBookDto ### CreateUpdateBookDto
在`Acme.BookStore.Application.Contracts`项目中创建一个名为 `CreateUpdateBookDto` 的DTO类: 在`Acme.BookStore.Application.Contracts`项目中创建 `Books` 文件夹(命名空间),并在其中添加名为 `CreateUpdateBookDto` 的DTO类:
````csharp ````csharp
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
@ -354,7 +358,7 @@ namespace Acme.BookStore.Books
```` ````
* 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息. * 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息.
* 它定义了数据注释属性(如`[Required]`)来定义属性的验证. DTO由ABP框架[自动验证](https://docs.abp.io/zh-Hans/abp/latest/Validation). * 它定义了数据注释特性(如`[Required]`)来定义属性的验证规则. DTO由ABP框架[自动验证](https://docs.abp.io/zh-Hans/abp/latest/Validation).
就像上面的`BookDto`一样,创建一个从`CreateUpdateBookDto`对象到`Book`实体的映射,最终映射配置类如下: 就像上面的`BookDto`一样,创建一个从`CreateUpdateBookDto`对象到`Book`实体的映射,最终映射配置类如下:
@ -378,7 +382,7 @@ namespace Acme.BookStore
### IBookAppService ### IBookAppService
下一步是为应用程序定义接口,在`Acme.BookStore.Application.Contracts`项目中定义一个名为`IBookAppService`的接口: 下一步是为应用程序定义接口,在`Acme.BookStore.Application.Contracts`项目创建 `Books` 文件夹(命名空间),并在其中添加名为`IBookAppService`的接口:
````csharp ````csharp
using System; using System;
@ -400,12 +404,12 @@ namespace Acme.BookStore.Books
```` ````
* 框架定义应用程序服务的接口**不是必需的**. 但是,它被建议作为最佳实践. * 框架定义应用程序服务的接口**不是必需的**. 但是,它被建议作为最佳实践.
* `ICrudAppService`定义了常见的**CRUD**方法:`GetAsync`,`GetListAsync`,`CreateAsync`,`UpdateAsync`和`DeleteAsync`. 你可以从空的`IApplicationService`接口继承并手动定义自己的方法(将在下一部分中完成). * `ICrudAppService`定义了常见的**CRUD**方法:`GetAsync`,`GetListAsync`,`CreateAsync`,`UpdateAsync`和`DeleteAsync`. 从这个接口扩展不是必需的,你可以从空的`IApplicationService`接口继承并手动定义自己的方法(将在下一部分中完成).
* `ICrudAppService`有一些变体, 你可以在每个方法中使用单独的DTO,也可以分别单独指定(例如使用不同的DTO进行创建和更新). * `ICrudAppService`有一些变体, 你可以在每个方法中使用单独的DTO(例如使用不同的DTO进行创建和更新).
### BookAppService ### BookAppService
在`Acme.BookStore.Application`项目中创建名为 `BookAppService``IBookAppService` 实现: 是时候实现`IBookAppService`接口了.在`Acme.BookStore.Application`项目中创建 `Books` 文件夹(命名空间),并在其中添加名为 `BookAppService` 的类:
````csharp ````csharp
using System; using System;
@ -439,17 +443,18 @@ namespace Acme.BookStore.Books
### 自动生成API Controllers ### 自动生成API Controllers
通常创建**Controller**以将应用程序服务公开为**HTTP API**端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. 在典型的ASP.NET Core应用程序中,你创建**API Controller**以将应用程序服务公开为**HTTP API**端点. 这将允许浏览器或第三方客户端通过HTTP调用它们.
ABP可以[**自动**](../API/Auto-API-Controllers.md)按照惯例将你的应用程序服务配置为MVC API控制器. ABP可以[**自动**](../API/Auto-API-Controllers.md)按照约定将你的应用程序服务配置为MVC API控制器.
### Swagger UI ### Swagger UI
启动模板配置为使用[Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)运行[swagger UI](https://swagger.io/tools/swagger-ui/). 运行应用程序并在浏览器中输入`https://localhost:XXXX/swagger/`(用你自己的端口替换XXXX)作为URL. 启动模板配置为使用[Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)运行[swagger UI](https://swagger.io/tools/swagger-ui/). 运行应用程序并在浏览器中输入`https://localhost:XXXX/swagger/`(用你自己的端口替换XXXX)作为URL.
使用`CTRL+F5`运行应用程序 ({{if UI=="MVC"}}`Acme.BookStore.Web`{{else}}`Acme.BookStore.HttpApi.Host`{{end}})并使用浏览器访问`https://localhost:<port>/swagger/` on your browser. 使用你自己的端口号替换 `<port>`.
你会看到一些内置的接口和`Book`的接口,它们都是REST风格的: 你会看到一些内置的服务端点和`Book`服务,它们都是REST风格的端点:
![bookstore-swagger](images/bookstore-swagger.png) ![bookstore-swagger](./images/bookstore-swagger.png)
Swagger有一个很好的UI来测试API. Swagger有一个很好的UI来测试API.

234
docs/zh-Hans/Tutorials/Part-2.md

@ -2,26 +2,10 @@
````json ````json
//[doc-params] //[doc-params]
{ {
"UI": ["MVC","NG"], "UI": ["MVC","Blazor","BlazorServer","NG"],
"DB": ["EF","Mongo"] "DB": ["EF","Mongo"]
} }
```` ````
{{
if UI == "MVC"
DB="ef"
DB_Text="Entity Framework Core"
UI_Text="mvc"
else if UI == "NG"
DB="mongodb"
DB_Text="MongoDB"
UI_Text="angular"
else
DB ="?"
UI_Text="?"
end
}}
## 关于本教程 ## 关于本教程
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: 在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:
@ -44,16 +28,30 @@ end
## 下载源码 ## 下载源码
本教程根据你的**UI** 和 **Database**偏好有多个版,我们准备了两种可供下载的源码组合: 本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合:
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) * [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) * [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
{{if UI == "MVC" && DB == "EF"}}
### 视频教程
本章也被录制为视频教程 **<a href="https://www.youtube.com/watch?v=UDNlLiPiBiw&list=PLsNclT2aHJcPNaCf7Io3DbMN6yAk_DgWJ&index=2" target="_blank">发布在YouTube</a>**.
{{end}}
{{if UI == "MVC"}} {{if UI == "MVC"}}
## 动态JavaScript代理 ## 动态JavaScript代理
通常在 **JavaScript** 端通过AJAX调用HTTP API端点. 你可以使用 `$.ajax` 或其他工具来调用端点. 但是ABP提供了更好的方法. **JavaScript** 端通过AJAX调用HTTP API端点是常见的做法. 你可以使用 `$.ajax` 或其他工具来调用端点. 但是ABP提供了更好的方法.
ABP**动态**为所有API端点创建 **[JavaScript代理](../UI/AspNetCore/Dynamic-JavaScript-Proxies.md)**. 所以你可以像调用**Javascript本地方法**一样使用任何**端点**. ABP**动态**为所有API端点创建 **[JavaScript代理](../UI/AspNetCore/Dynamic-JavaScript-Proxies.md)**. 所以你可以像调用**Javascript本地方法**一样使用任何**端点**.
@ -79,8 +77,6 @@ acme.bookStore.books.book.getList({}).done(function (result) { console.log(resul
![bookstore-getlist-result-network](images/bookstore-getlist-result-network.png) ![bookstore-getlist-result-network](images/bookstore-getlist-result-network.png)
Let's **create a new book** using the `create` function:
让我们使用 `create` 函数**创建一本书**: 让我们使用 `create` 函数**创建一本书**:
````js ````js
@ -94,6 +90,8 @@ acme.bookStore.books.book.create({
}); });
```` ````
> 如果你下载了本教程的源代码并按照示例中的步骤操作,你需要传递`authorId`参数给创建方法以**创建一本新书**.
您应该在控制台中看到类似以下的消息: 您应该在控制台中看到类似以下的消息:
````text ````text
@ -102,7 +100,7 @@ successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
检查数据库中的 `Books` 表你会看到新的一行. 你可以自己尝试使用 `get`, `update``delete` 函数. 检查数据库中的 `Books` 表你会看到新的一行. 你可以自己尝试使用 `get`, `update``delete` 函数.
我们将利用这些动态代理功能在接下来的章节来与服务器通信. 在接下来的章节,我们将利用这些动态代理函数与服务器通信.
{{end}} {{end}}
@ -112,7 +110,7 @@ successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
本地化文本位于 `Acme.BookStore.Domain.Shared` 项目的 `Localization/BookStore` 文件夹下: 本地化文本位于 `Acme.BookStore.Domain.Shared` 项目的 `Localization/BookStore` 文件夹下:
![bookstore-localization-files](./images/bookstore-localization-files-v2.png) ![bookstore-localization-files](images/bookstore-localization-files-v2.png)
打开 `en.json` (*英文翻译*)文件并更改内容,如下所示: 打开 `en.json` (*英文翻译*)文件并更改内容,如下所示:
@ -154,7 +152,7 @@ successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
* 为按钮项添加 `Menu:` 前缀. * 为按钮项添加 `Menu:` 前缀.
* 使用 `Enum:<enum-type>:<enum-value>` 命名约定来本地化枚举成员. 当您这样做时ABP可以在某些适当的情况下自动将枚举本地化. * 使用 `Enum:<enum-type>:<enum-value>` 命名约定来本地化枚举成员. 当您这样做时ABP可以在某些适当的情况下自动将枚举本地化.
如果未在本地化文件中定义文本,则文本将**回退**到本地化键(作为ASP.NET Core的标准行为). 如果未在本地化文件中定义文本,则文本将**回退**到本地化键(ASP.NET Core的标准行为).
> ABP本地化系统建立在[ASP.NET Core标准本地化](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization)系统之上,并以多种方式进行了扩展. 有关详细信息请参见[本地化文档](../Localization.md). > ABP本地化系统建立在[ASP.NET Core标准本地化](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization)系统之上,并以多种方式进行了扩展. 有关详细信息请参见[本地化文档](../Localization.md).
@ -162,7 +160,7 @@ successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
## 创建图书页面 ## 创建图书页面
是时候创建可见的和可用的东西了! 代替经典的MVC,我们将使用微软推荐的[Razor Pages UI](https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/razor-pages/razor-pages-start). 是时候创建可见的和可用的东西了! 我们将使用微软推荐的[Razor Pages UI](https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/razor-pages/razor-pages-start),而不是经典的MVC.
`Acme.BookStore.Web` 项目的 `Pages` 文件夹下创建一个名为新的 `Books` 的文件夹. 然后在文件夹右键选择 **添加 > Razor Page** 菜单. 输入名称 `Index`: `Acme.BookStore.Web` 项目的 `Pages` 文件夹下创建一个名为新的 `Books` 的文件夹. 然后在文件夹右键选择 **添加 > Razor Page** 菜单. 输入名称 `Index`:
@ -195,7 +193,7 @@ namespace Acme.BookStore.Web.Pages.Books
} }
``` ```
### 将Book页面添加到主菜单 ### 将图书页面添加到主菜单
打开 `Menus` 文件夹中的 `BookStoreMenuContributor` 类,在 `ConfigureMainMenuAsync` 方法的底部添加如下代码: 打开 `Menus` 文件夹中的 `BookStoreMenuContributor` 类,在 `ConfigureMainMenuAsync` 方法的底部添加如下代码:
@ -223,9 +221,7 @@ context.Menu.AddItem(
### 图书列表 ### 图书列表
We will use the [Datatables.net](https://datatables.net/) jQuery library to show the book list. Datatables library completely work via AJAX, it is fast, popular and provides a good user experience. 我们将使用[Datatables.net](https://datatables.net/)JQuery插件来显示图书列表. [Datatables](https://datatables.net/)可以完全通过AJAX工作,速度快,并提供良好的用户体验.
我们将使用[Datatables.net](https://datatables.net/)JQuery插件来显示页面上的表格列表. [Datatables](https://datatables.net/)可以完全通过AJAX工作,速度快,并提供良好的用户体验.
> Datatables插件在启动模板中配置,因此你可以直接在任何页面中使用它,无需在页面中引用样式和脚本文件. > Datatables插件在启动模板中配置,因此你可以直接在任何页面中使用它,无需在页面中引用样式和脚本文件.
@ -261,7 +257,7 @@ We will use the [Datatables.net](https://datatables.net/) jQuery library to show
`Pages/Books/` 文件夹中创建 `index.js`文件 `Pages/Books/` 文件夹中创建 `index.js`文件
![bookstore-index-js-file](./images/bookstore-index-js-file-v3.png) ![bookstore-index-js-file](images/bookstore-index-js-file-v3.png)
`index.js` 的内容如下: `index.js` 的内容如下:
@ -321,8 +317,8 @@ $(function () {
```` ````
* `abp.localization.getResource` 获取一个函数,该函数用于使用服务器端定义的相同JSON文件对文本进行本地化. 通过这种方式你可以与客户端共享本地化值. * `abp.localization.getResource` 获取一个函数,该函数用于使用服务器端定义的相同JSON文件对文本进行本地化. 通过这种方式你可以与客户端共享本地化值.
* `abp.libs.datatables.normalizeConfiguration`一个辅助方法.不是必须的, 但是它通过为缺少的选项提供常规值来简化数据表配置. * `abp.libs.datatables.normalizeConfiguration`是一个辅助方法.不是必须的, 但是它通过为缺省的选项提供约定的值来简化[Datatables](https://datatables.net/)配置.
* `abp.libs.datatables.createAjax`帮助ABP的动态JavaScript API代理跟[Datatable](https://datatables.net/)的格式相适应的辅助方法. * `abp.libs.datatables.createAjax`另一个辅助方法,用来适配ABP的动态JavaScript API代理和[Datatable](https://datatables.net/)期望的参数格式.
* `acme.bookStore.books.book.getList` 是动态JavaScript代理函数(上面已经介绍过了) * `acme.bookStore.books.book.getList` 是动态JavaScript代理函数(上面已经介绍过了)
* [luxon](https://moment.github.io/luxon/) 库也是该解决方案中预先配置的标准库,你可以轻松地执行日期/时间操作. * [luxon](https://moment.github.io/luxon/) 库也是该解决方案中预先配置的标准库,你可以轻松地执行日期/时间操作.
@ -334,15 +330,13 @@ $(function () {
![Book list](images/bookstore-book-list-3.png) ![Book list](images/bookstore-book-list-3.png)
这是一个完全正常工作的服务端分页,排序和本地化的图书列表. 这是一个可以正常工作的,服务端分页,排序和本地化的图书列表.
{{end}}
{{if UI == "NG"}} {{if UI == "NG"}}
## 安装NPM包 ## 安装NPM包
>注意: 本教程基于ABP Framework v3.0.3+. 如果你的项目版本较旧,请升级您的解决方案. 如果要升级现有的v2.x项目,请参阅[迁移指南](../UI/Angular/Migration-Guide-v3.md). >注意: 本教程基于ABP Framework v3.1.0+. 如果你的项目版本较旧,请升级您的解决方案. 如果要升级现有的v2.x项目,请参阅[迁移指南](../UI/Angular/Migration-Guide-v3.md).
`angular` 目录下打开命令行窗口,选择 `yarn` 命令安装NPM包: `angular` 目录下打开命令行窗口,选择 `yarn` 命令安装NPM包:
@ -357,9 +351,7 @@ yarn
- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) 用做UI组件库. - [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) 用做UI组件库.
- [ngx-datatable](https://swimlane.gitbook.io/ngx-datatable/) 用做 datatable 类库. - [ngx-datatable](https://swimlane.gitbook.io/ngx-datatable/) 用做 datatable 类库.
### BookModule 运行以下命令在angular应用程序根目录创建一个名为 `BookModule` 的新模块:
运行以下命令创建一个名为 `BookModule` 的新模块:
```bash ```bash
yarn ng generate module book --module app --routing --route books yarn ng generate module book --module app --routing --route books
@ -417,7 +409,7 @@ const routes: Routes = [
]; ];
```` ````
现在打开 `src/app/route.provider.ts` 以下替换 `configureRoutes` 函数: 现在打开 `src/app/route.provider.ts` 替换 `configureRoutes` 函数为以下代码:
```js ```js
function configureRoutes(routes: RoutesService) { function configureRoutes(routes: RoutesService) {
@ -453,24 +445,28 @@ function configureRoutes(routes: RoutesService) {
* `path` 路由的URL. * `path` 路由的URL.
* `name` 菜单项的名称(参阅[本地化文档](../UI/Angular/Localization.md)了解更多). * `name` 菜单项的名称(参阅[本地化文档](../UI/Angular/Localization.md)了解更多).
* `iconClass` 菜单项的图标(你可以使用默认的[Font Awesome](https://fontawesome.com/)图标). * `iconClass` 菜单项的图标(你可以使用默认的[Font Awesome](https://fontawesome.com/)图标).
* `order` 菜单项的排序.我们定义了101,它显示在 "Administration" 项的后面. * `order` 菜单项的排序.
* `layout` BooksModule路由的布局. 可以定义 `eLayoutType.application`, `eLayoutType.account``eLayoutType.empty`. * `layout` BooksModule路由的布局. (有三个预定义的布局类型: `eLayoutType.application`, `eLayoutType.account``eLayoutType.empty`).
更多信息请参阅[RoutesService 文档](../UI/Angular/Modifying-the-Menu.md#via-routesservice).
### 生成服务代理
更多信息请参阅[RoutesService 文档](https://docs.abp.io/en/abp/latest/UI/Angular/Modifying-the-Menu.md#via-routesservice). [ABP CLI](../CLI.md) 提供 `generate-proxy` 命令为HTTP APIs生成客户端代理.有了这些代理,在客户端使用HTTP APIs变得更加方便. 运行 `generate-proxy` 命令前, 你的 host 必须正在运行.
### 生成代理 > **警告**: 使用IIS Express时有一个问题; 它不允许从另一个进程连接应用程序. 如果你使用Visual Studio, 在运行按钮的下拉框中选择`Acme.BookStore.HttpApi.Host`,不要选择IIS Express, 如下图:
ABP CLI提供了 `generate-proxy` 命令为你的服务HTTP API生成客户端代理简化客户端使用服务的成本. 运行 `generate-proxy` 命令前你的host必须正在运行. 参阅 [CLI 文档](../CLI.md). ![vs-run-without-iisexpress](images/vs-run-without-iisexpress.png)
`angular` 文件夹下运行以下命令: 启动host应用程序后,`angular` 文件夹下运行以下命令:
```bash ```bash
abp generate-proxy -t ng abp generate-proxy -t ng
``` ```
生成的文件如下: 这个命令将在`/src/app/proxy/books`文件夹下产生以下文件:
![Generated files](./images/generated-proxies-2.png) ![Generated files](images/generated-proxies-3.png)
### BookComponent ### BookComponent
@ -479,8 +475,7 @@ abp generate-proxy -t ng
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core'; import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { BookDto } from './models'; import { BookService, BookDto } from '@proxy/books';
import { BookService } from './services';
@Component({ @Component({
selector: 'app-book', selector: 'app-book',
@ -494,7 +489,7 @@ export class BookComponent implements OnInit {
constructor(public readonly list: ListService, private bookService: BookService) {} constructor(public readonly list: ListService, private bookService: BookService) {}
ngOnInit() { ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getListByInput(query); const bookStreamCreator = (query) => this.bookService.getList(query);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response; this.book = response;
@ -503,8 +498,8 @@ export class BookComponent implements OnInit {
} }
``` ```
* 我们注入了生成的 `BookService`. * 我们引入并注入了生成的 `BookService`.
* 我们实现了 [ListService](https://docs.abp.io/en/abp/latest/UI/Angular/List-Service),它是一个公用服务,提供了简单的分页,排序和搜索. * 我们使用 [ListService](../UI/Angular/List-Service.md),它是一个工具服务,提供了易用的分页,排序和搜索.
打开 `/src/app/book/book.component.html` 用以下内容替换它: 打开 `/src/app/book/book.component.html` 用以下内容替换它:
@ -545,9 +540,138 @@ export class BookComponent implements OnInit {
现在你可以在浏览器看到最终结果: 现在你可以在浏览器看到最终结果:
![Book list final result](./images/bookstore-book-list.png) ![图书列表最终结果](images/bookstore-book-list.png)
{{end}} {{else if UI == "Blazor" || UI == "BlazorServer"}}
## 创建图书页面
是时候创建可见和可用的东西了! 右击`Acme.BookStore.Blazor`项目下的`Pages`文件夹,新建一个名为`Books.razor`的**razor组件**.
![blazor-add-books-component](images/blazor-add-books-component.png)
用以下内容替换这个组件的内容:
````html
@page "/books"
<h2>Books</h2>
@code {
}
````
### 将图书页面添加到主菜单
打开`Blazor`项目中的`BookStoreMenuContributor`类,在 `ConfigureMainMenuAsync` 方法的底部添加如下代码:
````csharp
context.Menu.AddItem(
new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
).AddItem(
new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/books"
)
)
);
````
运行项目,使用用户名 `admin` 和密码 `1q2w3E*` 登录到应用程序. 看到新菜单项已添加到顶部栏:
![blazor-menu-bookstore](images/blazor-menu-bookstore.png)
点击BookStore下的Books子菜单项就会跳转到空的图书页面.
### 图书列表
我们将使用[Blazorise library](https://blazorise.com/)作为UI组件.它是一个强大的库,支持主要的HTML/CSS框架,包括Bootstrap.
ABP提供了一个通用的基类,`AbpCrudPageBase<...>`,用来创建CRUD风格的页面.这个基类兼容用来构建`IBookAppService`的`ICrudAppService`.所以我们从`AbpCrudPageBase`继承,获得标准CRUD的默认实现.
打开`Books.razor` 并把内容修改成下面这样:
````xml
@page "/books"
@using Volo.Abp.Application.Dtos
@using Acme.BookStore.Books
@using Acme.BookStore.Localization
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<BookStoreResource> L
@inherits AbpCrudPageBase<IBookAppService, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>
<Card>
<CardHeader>
<h2>@L["Books"]</h2>
</CardHeader>
<CardBody>
<DataGrid TItem="BookDto"
Data="Entities"
ReadData="OnDataGridReadAsync"
TotalItems="TotalCount"
ShowPager="true"
PageSize="PageSize">
<DataGridColumns>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.Name)"
Caption="@L["Name"]"></DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.Type)"
Caption="@L["Type"]">
<DisplayTemplate>
@L[$"Enum:BookType:{(int)context.Type}"]
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.PublishDate)"
Caption="@L["PublishDate"]">
<DisplayTemplate>
@context.PublishDate.ToShortDateString()
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.Price)"
Caption="@L["Price"]">
</DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.CreationTime)"
Caption="@L["CreationTime"]">
<DisplayTemplate>
@context.CreationTime.ToLongDateString()
</DisplayTemplate>
</DataGridColumn>
</DataGridColumns>
</DataGrid>
</CardBody>
</Card>
````
> 如果你可以编译并运行成功,但看到一些语法错误.你可以忽略这些错误,因为Visual Studio处理Blazor还有一些bug.
* `AbpCrudPageBase<IBookAppService, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>`实现了所有的CRUD细节,我们从它继承.
* `Entities`, `TotalCount`, `PageSize`, `OnDataGridReadAsync`定义在基类中.
* 注入`IStringLocalizer<BookStoreResource>` (作为`L`对象),用于本地化.
虽然上面的代码非常容易理解,你仍然可以查看Blazorise [Card](https://blazorise.com/docs/components/card/)和[DataGrid](https://blazorise.com/docs/extensions/datagrid/)文档以更好地理解它们.
#### 关于AbpCrudPageBase
对于图书页面,我们将持续从`AbpCrudPageBase`获得益处. 你可以只注入`IBookAppService`并自己执行所有的服务端调用(感谢ABP的[动态C# HTTP API客户端代理](../API/Dynamic-CSharp-API-Clients.md)系统).
## 运行最终应用程序
你可以运行应用程序!该部分的最终用户界面如下所示:
![blazor-bookstore-book-list](images/blazor-bookstore-book-list.png)
这是一个可以正常工作的,服务端分页,排序和本地化的图书列表.
{{end # UI }}
## 下一章 ## 下一章

592
docs/zh-Hans/Tutorials/Part-3.md

@ -2,27 +2,10 @@
````json ````json
//[doc-params] //[doc-params]
{ {
"UI": ["MVC","NG"], "UI": ["MVC","Blazor","BlazorServer","NG"],
"DB": ["EF","Mongo"] "DB": ["EF","Mongo"]
} }
```` ````
{{
if UI == "MVC"
UI_Text="mvc"
else if UI == "NG"
UI_Text="angular"
else
UI_Text="?"
end
if DB == "EF"
DB_Text="Entity Framework Core"
else if DB == "Mongo"
DB_Text="MongoDB"
else
DB_Text="?"
end
}}
## 关于本教程 ## 关于本教程
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: 在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:
@ -45,16 +28,30 @@ end
## 下载源码 ## 下载源码
本教程根据你的**UI** 和 **Database**偏好有多个版,我们准备了两种可供下载的源码组合: 本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合:
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) * [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) * [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
{{if UI == "MVC" && DB == "EF"}}
### 视频教程
本章也被录制为视频教程 **<a href="https://www.youtube.com/watch?v=TLShZO8u2VE&list=PLsNclT2aHJcPNaCf7Io3DbMN6yAk_DgWJ&index=3" target="_blank">发布在YouTube</a>**.
{{end}}
{{if UI == "MVC"}} {{if UI == "MVC"}}
## 创建新书籍 ## 创建新书籍
通过本节, 你将会了解如何创建一个 modal form 来实现新增书籍的功能. 最终成果如下图所示: 通过本节, 你将会了解如何创建一个 modal form 实现新增书籍的功能. model dialog将如下图所示:
![bookstore-create-dialog](./images/bookstore-create-dialog-2.png) ![bookstore-create-dialog](./images/bookstore-create-dialog-2.png)
@ -66,7 +63,7 @@ end
#### CreateModal.cshtml.cs #### CreateModal.cshtml.cs
打开 `CreateModal.cshtml.cs` 代码文件,用如下代码替换 `CreateModalModel` 类的实现: 打开 `CreateModal.cshtml.cs` 代码文件(`CreateModalModel` 类),替换成以下代码:
````C# ````C#
using System.Threading.Tasks; using System.Threading.Tasks;
@ -101,10 +98,10 @@ namespace Acme.BookStore.Web.Pages.Books
} }
```` ````
* 该类派生于 `BookStorePageModel` 而非默认的 `PageModel`. `BookStorePageModel` 继承了 `PageModel` 并且添加了一些可以被你的page model类使用的通用属性和方法. * 该类派生于 `BookStorePageModel` 而非默认的 `PageModel`. `BookStorePageModel` 间接继承了 `PageModel` 并且添加了一些可以被你的page model类使用的通用属性和方法.
* `Book` 属性上的 `[BindProperty]` 特性将post请求提交上来的数据绑定到该属性上. * `Book` 属性上的 `[BindProperty]` 特性将post请求提交上来的数据绑定到该属性上.
* 该类通过构造函数注入了 `IBookAppService` 应用服务,并且在 `OnPostAsync` 处理程序中调用了服务的 `CreateAsync` 方法. * 该类通过构造函数注入了 `IBookAppService` 应用服务,并且在 `OnPostAsync` 处理程序中调用了服务的 `CreateAsync` 方法.
* 它在 `OnGet` 方法中创建一个新的 `CreateUpdateBookDto` 对象。 ASP.NET Core不需要像这样创建一个新实例就可以正常工作. 但是它不会为你创建实例,并且如果你的类在类构造函数中具有一些默认值分配或代码执行,它们将无法工作. 对于这种情况,我们为某些 `CreateUpdateBookDto` 属性设置了默认值. * 它在 `OnGet` 方法中创建一个新的 `CreateUpdateBookDto` 对象。 ASP.NET Core不需要像这样创建一个新实例就可以正常工作. 但是它不会为你创建实例,并且如果你的类在类构造函数中赋值一些默认值或执行一些代码,它们将无法工作. 对于这种情况,我们为某些 `CreateUpdateBookDto` 属性设置了默认值.
#### CreateModal.cshtml #### CreateModal.cshtml
@ -134,10 +131,9 @@ namespace Acme.BookStore.Web.Pages.Books
* 这个 modal 使用 `abp-dynamic-form` [tag Helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) 根据 `CreateBookViewModel` 类自动构建了表单. * 这个 modal 使用 `abp-dynamic-form` [tag Helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) 根据 `CreateBookViewModel` 类自动构建了表单.
* `abp-model` 指定了 `Book` 属性为模型对象. * `abp-model` 指定了 `Book` 属性为模型对象.
* `data-ajaxForm` 设置了表单通过AJAX提交,而不是经典的页面回发.
* `abp-form-content` tag helper 作为表单控件渲染位置的占位符 (这是可选的,只有你在 `abp-dynamic-form` 中像本示例这样添加了其他内容才需要). * `abp-form-content` tag helper 作为表单控件渲染位置的占位符 (这是可选的,只有你在 `abp-dynamic-form` 中像本示例这样添加了其他内容才需要).
> 提示: 就像在本示例中一样,`Layout` 应该为 `null`,因为当通过AJAX加载模态时,我们不希望包括所有布局. > 提示: 就像在本示例中一样,`Layout` 应该为 `null`,因为当通过AJAX加载模态窗口时,我们不希望包括所有布局.
### 添加 "New book" 按钮 ### 添加 "New book" 按钮
@ -195,9 +191,9 @@ namespace Acme.BookStore.Web.Pages.Books
如下图所示,只是在表格 **右上方** 添加了 **New book** 按钮: 如下图所示,只是在表格 **右上方** 添加了 **New book** 按钮:
![bookstore-new-book-button](./images/bookstore-new-book-button-2.png) ![bookstore-new-book-button](images/bookstore-new-book-button-2.png)
打开 `Pages/book/index.js``datatable` 配置代码后面添加如下代码: 打开 `Pages/Book/Index.js``datatable` 配置代码后面添加如下代码:
````js ````js
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
@ -212,9 +208,9 @@ $('#NewBookButton').click(function (e) {
}); });
```` ````
* `abp.ModalManager` 是一个在客户端打开和管理modal的辅助类.它基于Twitter Bootstrap的标准modal组件通过简化的API抽象隐藏了许多细节. * `abp.ModalManager` 是一个在客户端管理modal的辅助类.它内部使用了Twitter Bootstrap的标准modal组件,但通过简化的API抽象了许多细节.
* `createModal.onResult(...)` 用于在创建书籍后刷新数据表格. * `createModal.onResult(...)` 用于在创建书籍后刷新数据表格.
* `createModal.open();` 用于打开模态创建新书籍. * `createModal.open();` 用于打开modal创建新书籍.
`Index.js` 的内容最终如下所示: `Index.js` 的内容最终如下所示:
@ -335,8 +331,8 @@ namespace Acme.BookStore.Web.Pages.Books
} }
```` ````
* `[HiddenInput]``[BindProperty]` 是标准的 ASP.NET Core MVC 特性.这里启用 `SupportsGet` 从Http请求的查询字符串中获取Id的值. * `[HiddenInput]``[BindProperty]` 是标准的 ASP.NET Core MVC 特性.这里启用 `SupportsGet` 从Http请求的查询字符串参数中获取Id的值.
* 在 `OnGetAsync` 方法中,`BookAppService.GetAsync` 方法返回的 `BookDto` 映射成 `CreateUpdateBookDto` 并赋值给Book属性. * 在 `OnGetAsync` 方法中, 我们从 `BookAppService` 获得 `BookDto` ,并将它映射成DTO对象 `CreateUpdateBookDto`.
* `OnPostAsync` 方法直接使用 `BookAppService.UpdateAsync` 来更新实体. * `OnPostAsync` 方法直接使用 `BookAppService.UpdateAsync` 来更新实体.
### BookDto 到 CreateUpdateBookDto 对象映射 ### BookDto 到 CreateUpdateBookDto 对象映射
@ -391,10 +387,10 @@ namespace Acme.BookStore.Web
这个页面内容和 `CreateModal.cshtml` 非常相似,除了以下几点: 这个页面内容和 `CreateModal.cshtml` 非常相似,除了以下几点:
* 它包含`id`属性的`abp-input`, 用于存储编辑书的 `id` (它是隐藏的Input) * 它包含`id`属性的`abp-input`, 用于存储编辑书`id` (它是隐藏的Input)
* 此页面指定的post地址是`Books/EditModal`. * 此页面指定的post地址是`Books/EditModal`.
### 为表格添加 "操作(Actions)" 下拉菜单 ### 为表格添加 "操作(Actions)" 下拉菜单
我们将为表格每行添加下拉按钮 ("Actions"): 我们将为表格每行添加下拉按钮 ("Actions"):
@ -516,9 +512,9 @@ $(function () {
} }
```` ````
* `confirmMessage` 用来在实际执行 `action` 前向用户进行确认. * `confirmMessage` 执行 `action` 前向用户进行确认.
* 通过javascript代理方法 `acme.bookStore.books.book.delete(...)` 执行一个AJAX请求删除一个book实体. * `acme.bookStore.books.book.delete(...)` 执行一个AJAX请求删除一个book.
* `abp.notify.info` 用来在执行删除操作后显示一个toastr通知信息. * `abp.notify.info` 执行删除操作后显示一个通知信息.
由于我们使用了两个新的本地化文本(`BookDeletionConfirmationMessage`和`SuccesslyDeleted`),因此你需要将它们添加到本地化文件(`Acme.BookStore.Domain.Shared`项目的`Localization/BookStore`文件夹下的`en.json`): 由于我们使用了两个新的本地化文本(`BookDeletionConfirmationMessage`和`SuccesslyDeleted`),因此你需要将它们添加到本地化文件(`Acme.BookStore.Domain.Shared`项目的`Localization/BookStore`文件夹下的`en.json`):
@ -640,7 +636,7 @@ $(function () {
## 创建新书籍 ## 创建新书籍
下面的章节中,你将学习到如何创建一个新的模态对话框来新增书籍. 下面的章节中,你将学习到如何创建一个新的模态窗口新增书籍.
### BookComponent ### BookComponent
@ -649,8 +645,7 @@ $(function () {
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core'; import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { BookDto } from './models'; import { BookService, BookDto } from '@proxy/books';
import { BookService } from './services';
@Component({ @Component({
selector: 'app-book', selector: 'app-book',
@ -666,7 +661,7 @@ export class BookComponent implements OnInit {
constructor(public readonly list: ListService, private bookService: BookService) {} constructor(public readonly list: ListService, private bookService: BookService) {}
ngOnInit() { ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getListByInput(query); const bookStreamCreator = (query) => this.bookService.getList(query);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response; this.book = response;
@ -680,7 +675,8 @@ export class BookComponent implements OnInit {
} }
``` ```
* 我们定义了一个名为 `isModalOpen` 的变量和 `createBook` 方法. * 我们定义了一个名为 `isModalOpen` 的属性和 `createBook` 方法.
打开 `/src/app/book/book.component.html` 做以下更改: 打开 `/src/app/book/book.component.html` 做以下更改:
@ -726,11 +722,11 @@ export class BookComponent implements OnInit {
``` ```
* 添加了 `New book` 按钮到卡片头部. * 添加了 `New book` 按钮到卡片头部.
* 添加了 `abp-modal` 渲染模态框,允许用户创建新书. `abp-modal` 是显示模态框的预构建组件. 你也可以使用其它方法显示模态框,但 `abp-modal` 提供了一些附加的好处. * 添加了 `abp-modal` 渲染模态框,允许用户创建新书. `abp-modal` 是显示模态框的预构建组件. 你也可以使用其它方法显示模态框,但 `abp-modal` 提供了一些额外的好处.
你可以打开浏览器,点击**New book**按钮看到模态框. 你可以打开浏览器,点击**New book**按钮看到模态框.
![Empty modal for new book](./images/bookstore-empty-new-book-modal.png) ![Empty modal for new book](images/bookstore-empty-new-book-modal.png)
### 添加响应式表单 ### 添加响应式表单
@ -741,8 +737,7 @@ export class BookComponent implements OnInit {
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core'; import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { BookDto, BookType } from './models'; // add BookType import { BookService, BookDto, bookTypeOptions } from '@proxy/books'; // add bookTypeOptions
import { BookService } from './services';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // add this import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // add this
@Component({ @Component({
@ -756,12 +751,8 @@ export class BookComponent implements OnInit {
form: FormGroup; // add this line form: FormGroup; // add this line
bookType = BookType; // add this line
// add bookTypes as a list of BookType enum members // add bookTypes as a list of BookType enum members
bookTypes = Object.keys(this.bookType).filter( bookTypes = bookTypeOptions;
(key) => typeof this.bookType[key] === 'number'
);
isModalOpen = false; isModalOpen = false;
@ -772,7 +763,7 @@ export class BookComponent implements OnInit {
) {} ) {}
ngOnInit() { ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getListByInput(query); const bookStreamCreator = (query) => this.bookService.getList(query);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response; this.book = response;
@ -800,7 +791,7 @@ export class BookComponent implements OnInit {
return; return;
} }
this.bookService.createByInput(this.form.value).subscribe(() => { this.bookService.create(this.form.value).subscribe(() => {
this.isModalOpen = false; this.isModalOpen = false;
this.form.reset(); this.form.reset();
this.list.get(); this.list.get();
@ -809,12 +800,11 @@ export class BookComponent implements OnInit {
} }
``` ```
* 导入了 `FormGroup, FormBuilder and Validators`. * 从` @angular/forms `导入了 `FormGroup, FormBuilder and Validators`.
* 添加了 `form: FormGroup` 变量. * 添加了 `form: FormGroup` 变量.
* 添加了 `bookType` 属性,你可以从模板中获取 `BookType` 枚举成员.
* 添加了 `bookTypes` 属性作为 `BookType` 枚举成员列表. 将在表单选项中使用. * 添加了 `bookTypes` 属性作为 `BookType` 枚举成员列表. 将在表单选项中使用.
* 我们注入了 `fb: FormBuilder` 服务到构造函数. [FormBuilder](https://angular.io/api/forms/FormBuilder) 服务为生成控件提供了方便的方法. 它减少了构建复杂表单所需的样板文件的数量. * 我们注入了 `FormBuilder` 到构造函数. [FormBuilder](https://angular.io/api/forms/FormBuilder) 提供了简便的方法生成表单控件. 它减少了构建复杂表单所需的样板文件的数量.
* 我们添加了 `buildForm` 方法到文件末尾, 在 `createBook` 方法调用 `buildForm()` 方法. 该方法创建一个响应式表单去创建新书. * 我们添加了 `buildForm` 方法到文件末尾, 在 `createBook` 方法调用 `buildForm()` 方法.
* 添加了`save` 方法. * 添加了`save` 方法.
打开 `/src/app/book/book.component.html`,使用以下内容替换 `<ng-template #abpBody> </ng-template>`: 打开 `/src/app/book/book.component.html`,使用以下内容替换 `<ng-template #abpBody> </ng-template>`:
@ -836,7 +826,7 @@ export class BookComponent implements OnInit {
<label for="book-type">Type</label><span> * </span> <label for="book-type">Type</label><span> * </span>
<select class="form-control" id="book-type" formControlName="type"> <select class="form-control" id="book-type" formControlName="type">
<option [ngValue]="null">Select a book type</option> <option [ngValue]="null">Select a book type</option>
<option [ngValue]="bookType[type]" *ngFor="let type of bookTypes"> {%{{{ type }}}%}</option> <option [ngValue]="type.value" *ngFor="let type of bookTypes"> {%{{{ type.key }}}%}</option>
</select> </select>
</div> </div>
@ -897,13 +887,12 @@ export class BookModule { }
* 我们导入了 `NgbDatepickerModule` 来使用日期选择器. * 我们导入了 `NgbDatepickerModule` 来使用日期选择器.
打开 `/src/app/book/book.component.ts` 使用以内容替换: 打开 `/src/app/book/book.component.ts` 使用以内容替换:
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core'; import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { BookDto, BookType } from './models'; import { BookService, BookDto, bookTypeOptions } from '@proxy/books';
import { BookService } from './services';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
// added this line // added this line
@ -923,11 +912,7 @@ export class BookComponent implements OnInit {
form: FormGroup; form: FormGroup;
bookType = BookType; bookTypes = bookTypeOptions;
bookTypes = Object.keys(this.bookType).filter(
(key) => typeof this.bookType[key] === 'number'
);
isModalOpen = false; isModalOpen = false;
@ -938,7 +923,7 @@ export class BookComponent implements OnInit {
) {} ) {}
ngOnInit() { ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getListByInput(query); const bookStreamCreator = (query) => this.bookService.getList(query);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response; this.book = response;
@ -964,7 +949,7 @@ export class BookComponent implements OnInit {
return; return;
} }
this.bookService.createByInput(this.form.value).subscribe(() => { this.bookService.create(this.form.value).subscribe(() => {
this.isModalOpen = false; this.isModalOpen = false;
this.form.reset(); this.form.reset();
this.list.get(); this.list.get();
@ -974,11 +959,11 @@ export class BookComponent implements OnInit {
``` ```
* 导入了 `NgbDateNativeAdapter``NgbDateAdapter`. * 导入了 `NgbDateNativeAdapter``NgbDateAdapter`.
* 我们添加了一个新的 `NgbDateAdapter` 提供程序,它将Datepicker值转换为 `Date` 类型. 有关更多详细信息,请参见[datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview). * 我们添加了一个新的 `NgbDateAdapter` 提供程序,它将Datepicker值转换为 `Date` 类型. 更多详细信息,请参见[datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview).
现在你可以打开浏览器看到以下变化: 现在你可以打开浏览器看到以下变化:
![Save button to the modal](./images/bookstore-new-book-form-v2.png) ![Save button to the modal](images/bookstore-new-book-form-v2.png)
## 更新书籍 ## 更新书籍
@ -987,8 +972,7 @@ export class BookComponent implements OnInit {
```js ```js
import { ListService, PagedResultDto } from '@abp/ng.core'; import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { BookDto, BookType, CreateUpdateBookDto } from './models'; import { BookService, BookDto, bookTypeOptions } from '@proxy/books';
import { BookService } from './services';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
@ -1001,15 +985,11 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
export class BookComponent implements OnInit { export class BookComponent implements OnInit {
book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>; book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
selectedBook = new BookDto(); // declare selectedBook selectedBook = {} as BookDto; // declare selectedBook
form: FormGroup; form: FormGroup;
bookType = BookType; bookTypes = bookTypeOptions;
bookTypes = Object.keys(this.bookType).filter(
(key) => typeof this.bookType[key] === 'number'
);
isModalOpen = false; isModalOpen = false;
@ -1020,7 +1000,7 @@ export class BookComponent implements OnInit {
) {} ) {}
ngOnInit() { ngOnInit() {
const bookStreamCreator = (query) => this.bookService.getListByInput(query); const bookStreamCreator = (query) => this.bookService.getList(query);
this.list.hookToQuery(bookStreamCreator).subscribe((response) => { this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
this.book = response; this.book = response;
@ -1028,14 +1008,14 @@ export class BookComponent implements OnInit {
} }
createBook() { createBook() {
this.selectedBook = new BookDto(); // reset the selected book this.selectedBook = {} as BookDto; // reset the selected book
this.buildForm(); this.buildForm();
this.isModalOpen = true; this.isModalOpen = true;
} }
// Add editBook method // Add editBook method
editBook(id: string) { editBook(id: string) {
this.bookService.getById(id).subscribe((book) => { this.bookService.get(id).subscribe((book) => {
this.selectedBook = book; this.selectedBook = book;
this.buildForm(); this.buildForm();
this.isModalOpen = true; this.isModalOpen = true;
@ -1061,8 +1041,8 @@ export class BookComponent implements OnInit {
} }
const request = this.selectedBook.id const request = this.selectedBook.id
? this.bookService.updateByIdAndInput(this.form.value, this.selectedBook.id) ? this.bookService.update(this.selectedBook.id, this.form.value)
: this.bookService.createByInput(this.form.value); : this.bookService.create(this.form.value);
request.subscribe(() => { request.subscribe(() => {
this.isModalOpen = false; this.isModalOpen = false;
@ -1074,10 +1054,10 @@ export class BookComponent implements OnInit {
``` ```
* 我们声明了类型为 `BookDto``selectedBook` 变量. * 我们声明了类型为 `BookDto``selectedBook` 变量.
* 我们添加了 `editBook` 方法, 根据给定`Id` 设置 `selectedBook` 对象. * 我们添加了 `editBook` 方法, 根据给定书 `Id` 设置 `selectedBook` 对象.
* 我们替换了 `buildForm` 方法使用 `selectedBook` 数据创建表单. * 我们替换了 `buildForm` 方法使用 `selectedBook` 数据创建表单.
* 我们替换了 `createBook` 方法,设置 `selectedBook` 为空对象. * 我们替换了 `createBook` 方法,设置 `selectedBook` 为空对象.
* 我们替换了 `save` 方法. * 我们修改了 `save` 方法,同时处理新建和更新操作.
### 添加 "Actions" 下拉框到表格 ### 添加 "Actions" 下拉框到表格
@ -1111,7 +1091,7 @@ export class BookComponent implements OnInit {
在表格的第一列添加了一个 "Actions" 下拉菜单,如下图所示: 在表格的第一列添加了一个 "Actions" 下拉菜单,如下图所示:
![Action buttons](./images/bookstore-actions-buttons.png) ![Action buttons](images/bookstore-actions-buttons.png)
同时如下所示更改 `ng-template #abpHeader` 部分: 同时如下所示更改 `ng-template #abpHeader` 部分:
@ -1147,13 +1127,13 @@ constructor(
delete(id: string) { delete(id: string) {
this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure').subscribe((status) => { this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure').subscribe((status) => {
if (status === Confirmation.Status.confirm) { if (status === Confirmation.Status.confirm) {
this.bookService.deleteById(id).subscribe(() => this.list.get()); this.bookService.delete(id).subscribe(() => this.list.get());
} }
}); });
} }
``` ```
* 我们入了 `ConfirmationService`. * 我们入了 `ConfirmationService`.
* 我们注入了 `ConfirmationService` 到构造函数. * 我们注入了 `ConfirmationService` 到构造函数.
* 添加了 `delete` 方法. * 添加了 `delete` 方法.
@ -1161,6 +1141,7 @@ delete(id: string) {
### 添加删除按钮: ### 添加删除按钮:
打开 `/src/app/book/book.component.html` 修改 `ngbDropdownMenu` 添加删除按钮: 打开 `/src/app/book/book.component.html` 修改 `ngbDropdownMenu` 添加删除按钮:
```html ```html
@ -1174,11 +1155,442 @@ delete(id: string) {
最终操作下拉框UI看起来如下: 最终操作下拉框UI看起来如下:
![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png) ![bookstore-final-actions-dropdown](images/bookstore-final-actions-dropdown.png)
点击 `delete` 操作调用 `delete` 方法,然后显示一个确认弹层如下图所示.
![bookstore-confirmation-popup](images/bookstore-confirmation-popup.png)
{{end}}
{{if UI == "Blazor" || UI == "BlazorServer"}}
## 创建新书籍
通过本节, 你将会了解如何创建一个模态窗口实现新增书籍的功能. 因为我们已经从 `AbpCrudPageBase` 继承, 所以只需要开发视图部分.
### 添加 "New Button" 按钮
打开 `Books.razor` 替换 `<CardHeader>` 部分为以下代码:
````xml
<CardHeader>
<Row Class="justify-content-between">
<Column ColumnSize="ColumnSize.IsAuto">
<h2>@L["Books"]</h2>
</Column>
<Column ColumnSize="ColumnSize.IsAuto">
<Button Color="Color.Primary"
Clicked="OpenCreateModalAsync">@L["NewBook"]</Button>
</Column>
</Row>
</CardHeader>
````
如下图所示,卡片头 **右侧** 添加了 **New book** 按钮:
![blazor-add-book-button](images/blazor-add-book-button.png)
现在, 我们可以添加点击按钮后打开的模态窗口了.
### 书籍创建模态窗口
打开 `Books.razor`, 添加以下代码到页面底部:
````xml
<Modal @ref="@CreateModal">
<ModalBackdrop />
<ModalContent IsCentered="true">
<Form>
<ModalHeader>
<ModalTitle>@L["NewBook"]</ModalTitle>
<CloseButton Clicked="CloseCreateModalAsync"/>
</ModalHeader>
<ModalBody>
<Validations @ref="@CreateValidationsRef" Model="@NewEntity" ValidateOnLoad="false">
<Validation MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@L["Name"]</FieldLabel>
<TextEdit @bind-Text="@NewEntity.Name">
<Feedback>
<ValidationError/>
</Feedback>
</TextEdit>
</Field>
</Validation>
<Field>
<FieldLabel>@L["Type"]</FieldLabel>
<Select TValue="BookType" @bind-SelectedValue="@NewEntity.Type">
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType:{bookTypeValue}"]
</SelectItem>
}
</Select>
</Field>
<Field>
<FieldLabel>@L["PublishDate"]</FieldLabel>
<DateEdit TValue="DateTime" @bind-Date="NewEntity.PublishDate"/>
</Field>
<Field>
<FieldLabel>@L["Price"]</FieldLabel>
<NumericEdit TValue="float" @bind-Value="NewEntity.Price"/>
</Field>
</Validations>
</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary"
Clicked="CloseCreateModalAsync">@L["Cancel"]</Button>
<Button Color="Color.Primary"
Type="@ButtonType.Submit"
PreventDefaultOnSubmit="true"
Clicked="CreateEntityAsync">@L["Save"]</Button>
</ModalFooter>
</Form>
</ModalContent>
</Modal>
````
这段代码需要一个服务; 在文件顶部, `@inherits...` 行前, 注入 `AbpBlazorMessageLocalizerHelper<T>`:
````csharp
@inject AbpBlazorMessageLocalizerHelper<BookStoreResource> LH
````
* 表单实现了验证功能, `AbpBlazorMessageLocalizerHelper` 用于本地化验证消息.
* `CreateModal` 对象, `CloseCreateModalAsync``CreateEntityAsync` 方法定义在基类中. 参阅 [Blazorise文档](https://blazorise.com/docs/) 以深入理解 `Modal` 和其它组件.
这就是全部了. 运行应用程序, 尝试添加一本新书.
![blazor-new-book-modal](images/blazor-new-book-modal.png)
点击 `delete` 动作调用 `delete` 方法,然后无法显示一个确认弹层如下图所示. ## 更新书籍
编辑书籍与新建书籍很类似.
### 操作下拉菜单
![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png) 打开 `Books.razor` , 在 `DataGridColumns` 中添加以下 `DataGridEntityActionsColumn` 作为第一项:
````xml
<DataGridEntityActionsColumn TItem="BookDto" @ref="@EntityActionsColumn">
<DisplayTemplate>
<EntityActions TItem="BookDto" EntityActionsColumn="@EntityActionsColumn">
<EntityAction TItem="BookDto"
Text="@L["Edit"]"
Clicked="() => OpenEditModalAsync(context)" />
</EntityActions>
</DisplayTemplate>
</DataGridEntityActionsColumn>
````
* `OpenEditModalAsync` 定义在基类中, 它接收实体(书籍)参数, 编辑这个实体.
`DataGridEntityActionsColumn` 组件用于显示 `DataGrid` 每一行中的"操作" 下拉菜单. 如果其中只有唯一的操作, `DataGridEntityActionsColumn` 显示 **唯一按钮**, 而不是下拉菜单.
![blazor-edit-book-action](images/blazor-edit-book-action-2.png)
### 编辑模态窗口
我们现在可以定义一个模态窗口编辑书籍. 加入下面的代码到 `Books.razor` 页面的底部:
````xml
<Modal @ref="@EditModal">
<ModalBackdrop />
<ModalContent IsCentered="true">
<Form>
<ModalHeader>
<ModalTitle>@EditingEntity.Name</ModalTitle>
<CloseButton Clicked="CloseEditModalAsync"/>
</ModalHeader>
<ModalBody>
<Validations @ref="@EditValidationsRef" Model="@NewEntity" ValidateOnLoad="false">
<Validation MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@L["Name"]</FieldLabel>
<TextEdit @bind-Text="@EditingEntity.Name">
<Feedback>
<ValidationError/>
</Feedback>
</TextEdit>
</Field>
</Validation>
<Field>
<FieldLabel>@L["Type"]</FieldLabel>
<Select TValue="BookType" @bind-SelectedValue="@EditingEntity.Type">
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType:{bookTypeValue}"]
</SelectItem>
}
</Select>
</Field>
<Field>
<FieldLabel>@L["PublishDate"]</FieldLabel>
<DateEdit TValue="DateTime" @bind-Date="EditingEntity.PublishDate"/>
</Field>
<Field>
<FieldLabel>@L["Price"]</FieldLabel>
<NumericEdit TValue="float" @bind-Value="EditingEntity.Price"/>
</Field>
</Validations>
</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary"
Clicked="CloseEditModalAsync">@L["Cancel"]</Button>
<Button Color="Color.Primary"
Type="@ButtonType.Submit"
PreventDefaultOnSubmit="true"
Clicked="UpdateEntityAsync">@L["Save"]</Button>
</ModalFooter>
</Form>
</ModalContent>
</Modal>
````
### AutoMapper 配置
基类 `AbpCrudPageBase` 使用 [对象到对象映射](../Object-To-Object-Mapping.md) 系统将 `BookDto` 对象转化为`CreateUpdateBookDto` 对象. 因此, 我们需要定义映射.
打开 `Acme.BookStore.Blazor` 项目中的 `BookStoreBlazorAutoMapperProfile `, 替换成以下内容:
````csharp
using Acme.BookStore.Books;
using AutoMapper;
namespace Acme.BookStore.Blazor
{
public class BookStoreBlazorAutoMapperProfile : Profile
{
public BookStoreBlazorAutoMapperProfile()
{
CreateMap<BookDto, CreateUpdateBookDto>();
}
}
}
````
* `CreateMap<BookDto, CreateUpdateBookDto>();` 行用于定义映射.
### 测试编辑模态窗口
你可以运行程序并尝试编辑一本书.
![blazor-edit-book-modal](images/blazor-edit-book-modal.png)
> 提示: 尝试保留 *Name* 字段为空并提交表单, 将显示验证错误消息.
## 删除书籍
打开 `Books.razor` 页面, 在 `EntityActions` 中的"编辑" 操作下面加入以下的 `EntityAction`:
````xml
<EntityAction TItem="BookDto"
Text="@L["Delete"]"
Clicked="() => DeleteEntityAsync(context)"
ConfirmationMessage="() => GetDeleteConfirmationMessage(context)" />
````
* `DeleteEntityAsync` 定义在基类中. 通过向服务器发起请求删除实体.
* `ConfirmationMessage` 执行操作前显示确认消息的回调函数.
* `GetDeleteConfirmationMessage` 定义在基类中. 你可以覆写这个方法 (或传递其它值给 `ConfirmationMessage` 参数) 以定制本地化消息.
因为"操作" 按钮现在有了两个操作, 变成了下拉菜单:
![blazor-edit-book-action](images/blazor-delete-book-action.png)
运行程序并尝试删除一本书.
## 完整的 CRUD UI 代码
下面是完整的创建图书管理CRUD页面的代码, 这些代码在上面是分成两部分开发的:
````xml
@page "/books"
@using Volo.Abp.Application.Dtos
@using Acme.BookStore.Books
@using Acme.BookStore.Localization
@using Microsoft.Extensions.Localization
@using Volo.Abp.AspNetCore.Components.Web
@inject IStringLocalizer<BookStoreResource> L
@inject AbpBlazorMessageLocalizerHelper<BookStoreResource> LH
@inherits AbpCrudPageBase<IBookAppService, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>
<Card>
<CardHeader>
<Row Class="justify-content-between">
<Column ColumnSize="ColumnSize.IsAuto">
<h2>@L["Books"]</h2>
</Column>
<Column ColumnSize="ColumnSize.IsAuto">
<Button Color="Color.Primary"
Clicked="OpenCreateModalAsync">@L["NewBook"]</Button>
</Column>
</Row>
</CardHeader>
<CardBody>
<DataGrid TItem="BookDto"
Data="Entities"
ReadData="OnDataGridReadAsync"
CurrentPage="CurrentPage"
TotalItems="TotalCount"
ShowPager="true"
PageSize="PageSize">
<DataGridColumns>
<DataGridEntityActionsColumn TItem="BookDto" @ref="@EntityActionsColumn">
<DisplayTemplate>
<EntityActions TItem="BookDto" EntityActionsColumn="@EntityActionsColumn">
<EntityAction TItem="BookDto"
Text="@L["Edit"]"
Clicked="() => OpenEditModalAsync(context)" />
<EntityAction TItem="BookDto"
Text="@L["Delete"]"
Clicked="() => DeleteEntityAsync(context)"
ConfirmationMessage="()=>GetDeleteConfirmationMessage(context)" />
</EntityActions>
</DisplayTemplate>
</DataGridEntityActionsColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.Name)"
Caption="@L["Name"]"></DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.Type)"
Caption="@L["Type"]">
<DisplayTemplate>
@L[$"Enum:BookType:{(int) context.Type}"]
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.PublishDate)"
Caption="@L["PublishDate"]">
<DisplayTemplate>
@context.PublishDate.ToShortDateString()
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.Price)"
Caption="@L["Price"]">
</DataGridColumn>
<DataGridColumn TItem="BookDto"
Field="@nameof(BookDto.CreationTime)"
Caption="@L["CreationTime"]">
<DisplayTemplate>
@context.CreationTime.ToLongDateString()
</DisplayTemplate>
</DataGridColumn>
</DataGridColumns>
</DataGrid>
</CardBody>
</Card>
<Modal @ref="@CreateModal">
<ModalBackdrop />
<ModalContent IsCentered="true">
<Form>
<ModalHeader>
<ModalTitle>@L["NewBook"]</ModalTitle>
<CloseButton Clicked="CloseCreateModalAsync"/>
</ModalHeader>
<ModalBody>
<Validations @ref="@CreateValidationsRef" Model="@NewEntity" ValidateOnLoad="false">
<Validation MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@L["Name"]</FieldLabel>
<TextEdit @bind-Text="@NewEntity.Name">
<Feedback>
<ValidationError/>
</Feedback>
</TextEdit>
</Field>
</Validation>
<Field>
<FieldLabel>@L["Type"]</FieldLabel>
<Select TValue="BookType" @bind-SelectedValue="@NewEntity.Type">
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType:{bookTypeValue}"]
</SelectItem>
}
</Select>
</Field>
<Field>
<FieldLabel>@L["PublishDate"]</FieldLabel>
<DateEdit TValue="DateTime" @bind-Date="NewEntity.PublishDate"/>
</Field>
<Field>
<FieldLabel>@L["Price"]</FieldLabel>
<NumericEdit TValue="float" @bind-Value="NewEntity.Price"/>
</Field>
</Validations>
</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary"
Clicked="CloseCreateModalAsync">@L["Cancel"]</Button>
<Button Color="Color.Primary"
Type="@ButtonType.Submit"
PreventDefaultOnSubmit="true"
Clicked="CreateEntityAsync">@L["Save"]</Button>
</ModalFooter>
</Form>
</ModalContent>
</Modal>
<Modal @ref="@EditModal">
<ModalBackdrop />
<ModalContent IsCentered="true">
<Form>
<ModalHeader>
<ModalTitle>@EditingEntity.Name</ModalTitle>
<CloseButton Clicked="CloseEditModalAsync"/>
</ModalHeader>
<ModalBody>
<Validations @ref="@EditValidationsRef" Model="@NewEntity" ValidateOnLoad="false">
<Validation MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@L["Name"]</FieldLabel>
<TextEdit @bind-Text="@EditingEntity.Name">
<Feedback>
<ValidationError/>
</Feedback>
</TextEdit>
</Field>
</Validation>
<Field>
<FieldLabel>@L["Type"]</FieldLabel>
<Select TValue="BookType" @bind-SelectedValue="@EditingEntity.Type">
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType:{bookTypeValue}"]
</SelectItem>
}
</Select>
</Field>
<Field>
<FieldLabel>@L["PublishDate"]</FieldLabel>
<DateEdit TValue="DateTime" @bind-Date="EditingEntity.PublishDate"/>
</Field>
<Field>
<FieldLabel>@L["Price"]</FieldLabel>
<NumericEdit TValue="float" @bind-Value="EditingEntity.Price"/>
</Field>
</Validations>
</ModalBody>
<ModalFooter>
<Button Color="Color.Secondary"
Clicked="CloseEditModalAsync">@L["Cancel"]</Button>
<Button Color="Color.Primary"
Type="@ButtonType.Submit"
PreventDefaultOnSubmit="true"
Clicked="UpdateEntityAsync">@L["Save"]</Button>
</ModalFooter>
</Form>
</ModalContent>
</Modal>
````
{{end}} {{end}}

54
docs/zh-Hans/Tutorials/Part-4.md

@ -2,27 +2,10 @@
````json ````json
//[doc-params] //[doc-params]
{ {
"UI": ["MVC","NG"], "UI": ["MVC","Blazor","BlazorServer","NG"],
"DB": ["EF","Mongo"] "DB": ["EF","Mongo"]
} }
```` ````
{{
if UI == "MVC"
UI_Text="mvc"
else if UI == "NG"
UI_Text="angular"
else
UI_Text="?"
end
if DB == "EF"
DB_Text="Entity Framework Core"
else if DB == "Mongo"
DB_Text="MongoDB"
else
DB_Text="?"
end
}}
## 关于本教程 ## 关于本教程
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: 在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:
@ -45,16 +28,32 @@ end
## 下载源码 ## 下载源码
本教程根据你的**UI** 和 **Database**偏好有多个版,我们准备了两种可供下载的源码组合: 本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合:
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) * [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) * [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
{{if UI == "MVC" && DB == "EF"}}
### 视频教程
本章也被录制为视频教程 **<a href="https://www.youtube.com/watch?v=aidRB4YFDLM&list=PLsNclT2aHJcPNaCf7Io3DbMN6yAk_DgWJ&index=4" target="_blank">发布在YouTube</a>**.
{{end}}
## 解决方案中的测试项目 ## 解决方案中的测试项目
这一部分涵盖了 **服务器端** 测试. 解决方案中有多个测试项目: 这一部分涵盖了 **服务器端** 测试. 解决方案中有多个测试项目:
![bookstore-test-projects-v2](./images/bookstore-test-projects-{{UI_Text}}.png) ![bookstore-test-projects-v2](./images/bookstore-test-projects-mvc.png)
> 根据你选择的UI和数据库, 测试项目略微有所不同. 例如, 如果选择MongoDB, 那么 `Acme.BookStore.EntityFrameworkCore.Tests` 会变为 `Acme.BookStore.MongoDB.Tests`.
每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试: 每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试:
@ -64,11 +63,11 @@ end
{{if DB=="EF"}} {{if DB=="EF"}}
> 测试项目配置为使用 **SQLite内存** 作为数据库. 创建一个单独的数据库实例并使用数据种子系统进行初始化种子数据,为每个测试准备一个新的数据库. > 测试项目配置为使用 **SQLite内存** 作为数据库. 创建一个单独的数据库实例并使用[数据种子系统](../Data-Seeding.md)初始化种子数据,为每个测试准备一个新的数据库.
{{else if DB=="Mongo"}} {{else if DB=="Mongo"}}
> **[Mongo2Go](https://github.com/Mongo2Go/Mongo2Go)**库用于模拟MongoDB数据库. 创建一个单独的数据库实例并使用数据种子系统进行初始化种子数据,为每个测试准备一个新的数据库. > **[Mongo2Go](https://github.com/Mongo2Go/Mongo2Go)**库用于模拟MongoDB数据库. 创建一个单独的数据库实例并使用[数据种子系统](../Data-Seeding.md)初始化种子数据,为每个测试准备一个新的数据库.
{{end}} {{end}}
@ -78,12 +77,15 @@ end
## 测试 BookAppService ## 测试 BookAppService
`Acme.BookStore.Application.Tests` 项目中创建一个名叫 `BookAppService_Tests` 的测试类: `Acme.BookStore.Application.Tests` 项目`Books` 命名空间(文件夹)中创建一个名叫 `BookAppService_Tests` 的测试类:
````csharp ````csharp
using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Shouldly; using Shouldly;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Validation;
using Xunit; using Xunit;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books
@ -129,7 +131,7 @@ public async Task Should_Create_A_Valid_Book()
{ {
Name = "New test book 42", Name = "New test book 42",
Price = 10, Price = 10,
PublishDate = System.DateTime.Now, PublishDate = DateTime.Now,
Type = BookType.ScienceFiction Type = BookType.ScienceFiction
} }
); );
@ -211,7 +213,7 @@ namespace Acme.BookStore.Books
{ {
Name = "New test book 42", Name = "New test book 42",
Price = 10, Price = 10,
PublishDate = System.DateTime.Now, PublishDate = DateTime.Now,
Type = BookType.ScienceFiction Type = BookType.ScienceFiction
} }
); );
@ -244,7 +246,7 @@ namespace Acme.BookStore.Books
} }
```` ````
打开**测试资源管理器**(测试 -> Windows -> 测试资源管理器)并**执行**所有测试: 打开**测试资源管理器**(测试 -> Windows -> 测试资源管理器)并**执行所有**测试:
![bookstore-appservice-tests](./images/bookstore-appservice-tests.png) ![bookstore-appservice-tests](./images/bookstore-appservice-tests.png)

636
docs/zh-Hans/Tutorials/Part-5.md

@ -1 +1,635 @@
TODO.. # Web应用程序开发教程 - 第五章: 授权
````json
//[doc-params]
{
"UI": ["MVC","Blazor","BlazorServer","NG"],
"DB": ["EF","Mongo"]
}
````
## 关于本教程
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:
* **{{DB_Text}}** 做为ORM提供程序.
* **{{UI_Value}}** 做为UI框架.
本教程分为以下部分:
- [Part 1: 创建服务端](Part-1.md)
- [Part 2: 图书列表页面](Part-2.md)
- [Part 3: 创建,更新和删除图书](Part-2.md)
- [Part 4: 集成测试](Part-4.md)
- **Part 5: 授权**(本章)
- [Part 6: 作者: 领域层](Part-6.md)
- [Part 7: 作者: 数据库集成](Part-7.md)
- [Part 8: 作者: 应用服务层](Part-8.md)
- [Part 9: 作者: 用户页面](Part-9.md)
- [Part 10: 图书到作者的关系](Part-10.md)
## 下载源码
本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合:
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
{{if UI == "MVC" && DB == "EF"}}
### 视频教程
本章也被录制为视频教程 **<a href="https://www.youtube.com/watch?v=1WsfMITN_Jk&list=PLsNclT2aHJcPNaCf7Io3DbMN6yAk_DgWJ&index=5" target="_blank">发布在YouTube</a>**.
{{end}}
## 权限
ABP框架提供了一个基于ASP.NET Core[授权基础架构](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction)的[授权系统](../Authorization.md). 基于标准授权基础架构的一个主要功能是添加了 **权限系统**, 这个系统允许定义权限并且根据角色, 用户或客户端启用/禁用权限.
### 权限名称
权限必须有唯一的名称 (一个 `字符串`). 最好的方法是把它定义为一个 `常量`, 这样我们就可以重用这个权限名称了.
打开 `Acme.BookStore.Application.Contracts` 项目中的 `BookStorePermissions` 类 (位于 `Permissions` 文件夹) 并替换为以下代码:
````csharp
namespace Acme.BookStore.Permissions
{
public static class BookStorePermissions
{
public const string GroupName = "BookStore";
public static class Books
{
public const string Default = GroupName + ".Books";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
}
}
````
权限名称具有层次结构. 例如, "创建图书" 权限被定义为 `BookStore.Books.Create`. ABP不强制必须如此, 但这是一种有益的做法.
### 权限定义
在使用权限前必须定义它们.
打开 `Acme.BookStore.Application.Contracts` 项目中的 `BookStorePermissionDefinitionProvider` 类 (位于 `Permissions` 文件夹) 并替换为以下代码:
````csharp
using Acme.BookStore.Localization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Localization;
namespace Acme.BookStore.Permissions
{
public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore"));
var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books"));
booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create"));
booksPermission.AddChild(BookStorePermissions.Books.Edit, L("Permission:Books.Edit"));
booksPermission.AddChild(BookStorePermissions.Books.Delete, L("Permission:Books.Delete"));
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<BookStoreResource>(name);
}
}
}
````
这个类定义了一个 **权限组** (在UI上分组权限, 下文会看到) 和 权限组中的**4个权限**. 而且, **创建**, **编辑****删除**`BookStorePermissions.Books.Default` 权限的子权限. **仅当父权限被选择**时, 子权限才能被选择.
最后, 编辑本地化文件 (`Acme.BookStore.Domain.Shared` 项目的 `Localization/BookStore` 文件夹中的 `en.json`) 定义上面使用的本地化键:
````json
"Permission:BookStore": "Book Store",
"Permission:Books": "Book Management",
"Permission:Books.Create": "Creating new books",
"Permission:Books.Edit": "Editing the books",
"Permission:Books.Delete": "Deleting the books"
````
> 本地化键名可以是任意的, 并没有强制的规则. 但我们推荐上面使用的约定.
### 权限管理界面
完成权限定义后, 可以在**权限管理模态窗口**看到它们.
在*管理 -> Identity -> 角色* 页面, 选择admin角色的 *权限* 操作, 打开权限管理模态窗口:
![bookstore-permissions-ui](images/bookstore-permissions-ui.png)
授予你希望的权限并保存.
> **提示**: 如果运行 `Acme.BookStore.DbMigrator` 应用程序, 新权限会被自动授予admin.
## 授权
现在, 你可以使用权限授权图书管理.
### 应用层 和 HTTP API
打开 the `BookAppService` 类, 设置策略名称为上面定义的权限名称.
````csharp
using System;
using Acme.BookStore.Permissions;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Books
{
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{
public BookAppService(IRepository<Book, Guid> repository)
: base(repository)
{
GetPolicyName = BookStorePermissions.Books.Default;
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Delete;
}
}
}
````
加入代码到构造器. 基类中的 `CrudAppService` 自动在CRUD操作中使用这些权限. 这不仅实现了 **应用服务** 的安全性, 也实现了 **HTTP API** 安全性, 因为如前解释的, HTTP API 自动使用这些服务. (参阅 [自动 API controllers](../API/Auto-API-Controllers.md)).
> 在稍后开发作者管理功能时, 你将会看到声明式授权, 使用 `[Authorize(...)]` 特性.
{{if UI == "MVC"}}
### Razor 页面
虽然安全的 HTTP API和应用服务阻止未授权用户使用服务, 但他们依然可以导航到图书管理页面. 虽然当页面发起第一个访问服务器的AJAX请求时会收到授权异常, 但为了更好的用户体验和安全性, 我们应该对页面进行授权.
打开 `BookStoreWebModule``ConfigureServices` 方法中加入以下代码:
````csharp
Configure<RazorPagesOptions>(options =>
{
options.Conventions.AuthorizePage("/Books/Index", BookStorePermissions.Books.Default);
options.Conventions.AuthorizePage("/Books/CreateModal", BookStorePermissions.Books.Create);
options.Conventions.AuthorizePage("/Books/EditModal", BookStorePermissions.Books.Edit);
});
````
现在未授权用户会被重定向至**登录页面**.
#### 隐藏新建图书按钮
图书管理页面有一个 *新建图书* 按钮, 当用户没有 *图书新建* 权限时就不可见的.
![bookstore-new-book-button-small](images/bookstore-new-book-button-small.png)
打开 the `Pages/Books/Index.cshtml` 文件, 替换内容为以下代码:
````html
@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Permissions
@using Acme.BookStore.Web.Pages.Books
@using Microsoft.AspNetCore.Authorization
@using Microsoft.Extensions.Localization
@model IndexModel
@inject IStringLocalizer<BookStoreResource> L
@inject IAuthorizationService AuthorizationService
@section scripts
{
<abp-script src="/Pages/Books/Index.js"/>
}
<abp-card>
<abp-card-header>
<abp-row>
<abp-column size-md="_6">
<abp-card-title>@L["Books"]</abp-card-title>
</abp-column>
<abp-column size-md="_6" class="text-right">
@if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create))
{
<abp-button id="NewBookButton"
text="@L["NewBook"].Value"
icon="plus"
button-type="Primary"/>
}
</abp-column>
</abp-row>
</abp-card-header>
<abp-card-body>
<abp-table striped-rows="true" id="BooksTable"></abp-table>
</abp-card-body>
</abp-card>
````
* 加入 `@inject IAuthorizationService AuthorizationService` 以访问授权服务.
* 使用 `@if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create))` 检查图书创建权限, 条件显示 *新建图书* 按钮.
### JavaScript端
图书管理页面中的图书表格每行都有操作按钮. 操作按钮包括 *编辑**删除* 操作:
![bookstore-edit-delete-actions](images/bookstore-edit-delete-actions.png)
如果用户没有权限, 应该隐藏相关的操作. 表格行中的操作有一个 `visible` 属性, 可以设置为 `false` 隐藏操作项.
打开 `Acme.BookStore.Web` 项目中的 `Pages/Books/Index.js`, 为 `编辑` 操作加入 `visible` 属性:
````js
{
text: l('Edit'),
visible: abp.auth.isGranted('BookStore.Books.Edit'), //CHECK for the PERMISSION
action: function (data) {
editModal.open({ id: data.record.id });
}
}
````
`Delete` 操作进行同样的操作:
````js
visible: abp.auth.isGranted('BookStore.Books.Delete')
````
* `abp.auth.isGranted(...)` 检查前面定义的权限.
* `visible` 也可以是一个返回 `bool` 值的函数. 这个函数可以稍后根据某些条件计算.
### 菜单项
即使我们在图书管理页面的所有层都控制了权限, 应用程序的主菜单依然会显示. 我们应该隐藏用户没有权限的菜单项.
打开 `BookStoreMenuContributor` 类, 找到下面的代码:
````csharp
context.Menu.AddItem(
new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
).AddItem(
new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/Books"
)
)
);
````
替换为以下代码:
````csharp
var bookStoreMenu = new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
);
context.Menu.AddItem(bookStoreMenu);
//CHECK the PERMISSION
if (await context.IsGrantedAsync(BookStorePermissions.Books.Default))
{
bookStoreMenu.AddItem(new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/Books"
));
}
````
你需要为 `ConfigureMenuAsync` 方法加入 `async` 关键字, 并重新组织返回值. 最终的 `BookStoreMenuContributor` 类应该如下:
````csharp
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Acme.BookStore.Localization;
using Acme.BookStore.MultiTenancy;
using Acme.BookStore.Permissions;
using Volo.Abp.TenantManagement.Web.Navigation;
using Volo.Abp.UI.Navigation;
namespace Acme.BookStore.Web.Menus
{
public class BookStoreMenuContributor : IMenuContributor
{
public async Task ConfigureMenuAsync(MenuConfigurationContext context)
{
if (context.Menu.Name == StandardMenus.Main)
{
await ConfigureMainMenuAsync(context);
}
}
private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
{
if (!MultiTenancyConsts.IsEnabled)
{
var administration = context.Menu.GetAdministration();
administration.TryRemoveMenuItem(TenantManagementMenuNames.GroupName);
}
var l = context.GetLocalizer<BookStoreResource>();
context.Menu.Items.Insert(0, new ApplicationMenuItem("BookStore.Home", l["Menu:Home"], "~/"));
var bookStoreMenu = new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
);
context.Menu.AddItem(bookStoreMenu);
//CHECK the PERMISSION
if (await context.IsGrantedAsync(BookStorePermissions.Books.Default))
{
bookStoreMenu.AddItem(new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/Books"
));
}
}
}
}
````
{{else if UI == "NG"}}
### Angular Guard 配置
UI的第一步是防止未认证用户看见"图书"菜单项并进入图书管理页面.
打开 `/src/app/book/book-routing.module.ts` 替换为以下代码:
````js
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard, PermissionGuard } from '@abp/ng.core';
import { BookComponent } from './book.component';
const routes: Routes = [
{ path: '', component: BookComponent, canActivate: [AuthGuard, PermissionGuard] },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class BookRoutingModule {}
````
* 从 `@abp/ng.core` 引入 `AuthGuard``PermissionGuard`.
* 在路由定义中添加 `canActivate: [AuthGuard, PermissionGuard]`.
打开 `/src/app/route.provider.ts`, 在 `/books` 路由中添加 `requiredPolicy: 'BookStore.Books'`. `/books` 路由应该如以下配置:
````js
{
path: '/books',
name: '::Menu:Books',
parentName: '::Menu:BookStore',
layout: eLayoutType.application,
requiredPolicy: 'BookStore.Books',
}
````
### 隐藏新建图书按钮
当用户没有 *图书新建* 权限时, 图书管理页面上的 *新建图书* 按钮应该不可见.
![bookstore-new-book-button-small](images/bookstore-new-book-button-small.png)
打开 `/src/app/book/book.component.html` 文件, 替换创建按钮的HTML内容如下:
````html
<!-- Add the abpPermission directive -->
<button *abpPermission="'BookStore.Books.Create'" id="create" class="btn btn-primary" type="button" (click)="createBook()">
<i class="fa fa-plus mr-1"></i>
<span>{%{{{ '::NewBook' | abpLocalization }}}%}</span>
</button>
````
* 加入 `*abpPermission="'BookStore.Books.Create'"`, 当用户没有权限时隐藏按钮.
### 隐藏编辑和删除操作
图书管理页面中的图书表格每行都有操作按钮. 操作按钮包括 *编辑**删除* 操作:
![bookstore-edit-delete-actions](images/bookstore-edit-delete-actions.png)
如果用户没有权限, 应该隐藏相关的操作.
打开 `/src/app/book/book.component.html` 文件, 替换编辑和删除按钮的内容如下:
````html
<!-- Add the abpPermission directive -->
<button *abpPermission="'BookStore.Books.Edit'" ngbDropdownItem (click)="editBook(row.id)">
{%{{{ '::Edit' | abpLocalization }}}%}
</button>
<!-- Add the abpPermission directive -->
<button *abpPermission="'BookStore.Books.Delete'" ngbDropdownItem (click)="delete(row.id)">
{%{{{ '::Delete' | abpLocalization }}}%}
</button>
````
* 加入 `*abpPermission="'BookStore.Books.Create'"`, 当用户没有编辑权限时隐藏按钮.
* 加入 `*abpPermission="'BookStore.Books.Delete'"`, 当用户没有删除权限时隐藏按钮.
{{else if UI == "Blazor"}}
### Razor验证组件
打开 `Acme.BookStore.Blazor` 项目中的 `/Pages/Books.razor` 文件, 在` @page` 指令和命名空间引入(`@using` 行)后添加 `Authorize` 特性, 如下所示:
````html
@page "/books"
@attribute [Authorize(BookStorePermissions.Books.Default)]
@using Acme.BookStore.Permissions
@using Microsoft.AspNetCore.Authorization
...
````
添加这个特性阻止未登录用户或未授权用户访问这个页面. 用户重试后, 会被重定向到登录页面.
### 显示/隐藏操作
图书管理页面上的每一种图书都有 *新建* 按钮和 *编辑*, *删除* 操作. 如果用户没有相关权限, 这些按钮/操作应该被隐藏.
基类 `AbpCrudPageBase` 已经具有这些操作需要的功能.
#### 设置策略 (权限) 名称
加入以下代码到 `Books.razor` 文件结尾:
````csharp
@code
{
public Books() // Constructor
{
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Delete;
}
}
````
基类 `AbpCrudPageBase` 自动检查相关操作的权限. 如果需要手动检查, 它也定义了相应的属性.
* `HasCreatePermission`: True, 如果用户具有新建实体的权限.
* `HasUpdatePermission`: True, 如果用户具有编辑/更新实体的权限.
* `HasDeletePermission`: True, 如果用户具有删除实体的权限.
> **Blazor 提示**: 当添加少量代码到 `@code` 是没有问题的. 当添加的代码变长时, 建议使用代码后置方法以便于维护. 我们将在作者部分使用这个方法.
#### 隐藏新建图书按钮
检查 *新建图书* 按钮权限:
````xml
@if (HasCreatePermission)
{
<Button Color="Color.Primary"
Clicked="OpenCreateModalAsync">@L["NewBook"]</Button>
}
````
#### 隐藏编辑/删除操作
`EntityAction` 组件定义了 `Visible` 属性 (参数) 以条件显示操作.
更新 `EntityActions` 部分:
````xml
<EntityActions TItem="BookDto" EntityActionsColumn="@EntityActionsColumn">
<EntityAction TItem="BookDto"
Text="@L["Edit"]"
Visible=HasUpdatePermission
Clicked="() => OpenEditModalAsync(context)" />
<EntityAction TItem="BookDto"
Text="@L["Delete"]"
Visible=HasDeletePermission
Clicked="() => DeleteEntityAsync(context)"
ConfirmationMessage="()=>GetDeleteConfirmationMessage(context)" />
</EntityActions>
````
#### 关于权限缓存
你可以运行和测试权限. 从admin角色中移除一个图书相关权限, 观察到相关按钮/操作从UI上消失.
在客户端, **ABP框架缓存当前用户的权限** . 所以, 当你修改了你的权限, 你需要手工 **刷新页面**. 如果不刷新并试图使用被禁的操作, 你会从服务器收到一个HTTP 403 (forbidden) 响应.
> 修改角色或用户的权限在服务端立即生效. 所以, 缓存系统不会导致安全问题.
### 菜单项
即使我们在图书管理页面的所有层都控制了权限, 应用程序的主菜单依然会显示. 我们应该隐藏用户没有权限的菜单项.
打开 `Acme.BookStore.Blazor` 项目中的 `BookStoreMenuContributor` 类, 找到以下代码:
````csharp
context.Menu.AddItem(
new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
).AddItem(
new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/books"
)
)
);
````
替换为以下代码:
````csharp
var bookStoreMenu = new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
);
context.Menu.AddItem(bookStoreMenu);
//CHECK the PERMISSION
if (await context.IsGrantedAsync(BookStorePermissions.Books.Default))
{
bookStoreMenu.AddItem(new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/books"
));
}
````
你需要为 `ConfigureMenuAsync` 方法加入 `async` 关键字并重新整理返回值. 最终的 `ConfigureMainMenuAsync` 方法如下:
````csharp
private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
{
var l = context.GetLocalizer<BookStoreResource>();
context.Menu.Items.Insert(
0,
new ApplicationMenuItem(
"BookStore.Home",
l["Menu:Home"],
"/",
icon: "fas fa-home"
)
);
var bookStoreMenu = new ApplicationMenuItem(
"BooksStore",
l["Menu:BookStore"],
icon: "fa fa-book"
);
context.Menu.AddItem(bookStoreMenu);
//CHECK the PERMISSION
if (await context.IsGrantedAsync(BookStorePermissions.Books.Default))
{
bookStoreMenu.AddItem(new ApplicationMenuItem(
"BooksStore.Books",
l["Menu:Books"],
url: "/books"
));
}
}
````
{{end}}
## 下一章
查看本教程的[下一章](Part-6.md).

279
docs/zh-Hans/Tutorials/Part-6.md

@ -1 +1,278 @@
TODO.. # Web应用程序开发教程 - 第六章: 作者: 领域层
````json
//[doc-params]
{
"UI": ["MVC","Blazor","BlazorServer","NG"],
"DB": ["EF","Mongo"]
}
````
## 关于本教程
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:
* **{{DB_Text}}** 做为ORM提供程序.
* **{{UI_Value}}** 做为UI框架.
本教程分为以下部分:
- [Part 1: 创建服务端](Part-1.md)
- [Part 2: 图书列表页面](Part-2.md)
- [Part 3: 创建,更新和删除图书](Part-2.md)
- [Part 4: 集成测试](Part-4.md)
- [Part 5: 授权](Part-5.md)
- **Part 6: 作者: 领域层**(本章)
- [Part 7: 作者: 数据库集成](Part-7.md)
- [Part 8: 作者: 应用服务层](Part-8.md)
- [Part 9: 作者: 用户页面](Part-9.md)
- [Part 10: 图书到作者的关系](Part-10.md)
## 下载源码
本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合:
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
## 简介
在前面的章节中, 我们使用 ABP 框架轻松地构建了一些服务;
* 使用 [CrudAppService](../Application-Services.md) 基类, 而不是为标准的增删改查操作手工开发应用服务.
* 使用 [generic repositories](../Repositories.md) 自动完成数据层功能.
对于 "作者" 部分;
* 我们将要展示在需要的情况下, 如何 **手工做一些事情**.
* 我们将要实现一些 **领域驱动设计 (DDD) 最佳实践**.
> **开发将会逐层完成, 一次聚焦一层. 在真实项目中, 你会逐个功能(垂直)开发, 如同前面的教程. 通过这种方式, 你可以体验这两种方式**
## 作者实体
`Acme.BookStore.Domain` 项目中创建 `Authors` 文件夹 (命名空间), 在其中加入 `Author` 类:
````csharp
using System;
using JetBrains.Annotations;
using Volo.Abp;
using Volo.Abp.Domain.Entities.Auditing;
namespace Acme.BookStore.Authors
{
public class Author : FullAuditedAggregateRoot<Guid>
{
public string Name { get; private set; }
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
private Author()
{
/* This constructor is for deserialization / ORM purpose */
}
internal Author(
Guid id,
[NotNull] string name,
DateTime birthDate,
[CanBeNull] string shortBio = null)
: base(id)
{
SetName(name);
BirthDate = birthDate;
ShortBio = shortBio;
}
internal Author ChangeName([NotNull] string name)
{
SetName(name);
return this;
}
private void SetName([NotNull] string name)
{
Name = Check.NotNullOrWhiteSpace(
name,
nameof(name),
maxLength: AuthorConsts.MaxNameLength
);
}
}
}
````
* 由 `FullAuditedAggregateRoot<Guid>` 继承使得实体支持[软删除](../Data-Filtering.md) (指实体被删除时, 它并没有从数据库中被删除, 而只是被标记删除), 实体也具有了 [审计](../Entities.md) 属性.
* `Name` 属性的 `private set` 限制从类的外部设置这个属性. 有两种方法设置名字 (两种都进行了验证):
* 当新建一个作者时, 通过构造器.
* 使用 `ChangeName` 方法更新名字.
* `构造器``ChangeName` 方法的访问级别是 `internal`, 强制这些方法只能在领域层由 `AuthorManager` 使用. 稍后将对此进行解释.
* `Check` 类是一个ABP框架工具类, 用于检查方法参数 (如果参数非法会抛出 `ArgumentException`).
`AuthorConsts` 是一个简单的类, 它位于 `Acme.BookStore.Domain.Shared` 项目的 `Authors` 命名空间 (文件夹)中:
````csharp
namespace Acme.BookStore.Authors
{
public static class AuthorConsts
{
public const int MaxNameLength = 64;
}
}
````
`Acme.BookStore.Domain.Shared` 项目中创建这个类, 因为[数据传输类](../Data-Transfer-Objects.md) (DTOs) 稍后会再一次用到它.
## AuthorManager: 领域服务
`Author` 构造器和 `ChangeName` 方法的访问级别是 `internal`, 所以它们只能在领域层使用. 在 `Acme.BookStore.Domain` 项目中的 `Authors` 文件夹 (命名空间)创建 `AuthorManager` 类:
````csharp
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Volo.Abp;
using Volo.Abp.Domain.Services;
namespace Acme.BookStore.Authors
{
public class AuthorManager : DomainService
{
private readonly IAuthorRepository _authorRepository;
public AuthorManager(IAuthorRepository authorRepository)
{
_authorRepository = authorRepository;
}
public async Task<Author> CreateAsync(
[NotNull] string name,
DateTime birthDate,
[CanBeNull] string shortBio = null)
{
Check.NotNullOrWhiteSpace(name, nameof(name));
var existingAuthor = await _authorRepository.FindByNameAsync(name);
if (existingAuthor != null)
{
throw new AuthorAlreadyExistsException(name);
}
return new Author(
GuidGenerator.Create(),
name,
birthDate,
shortBio
);
}
public async Task ChangeNameAsync(
[NotNull] Author author,
[NotNull] string newName)
{
Check.NotNull(author, nameof(author));
Check.NotNullOrWhiteSpace(newName, nameof(newName));
var existingAuthor = await _authorRepository.FindByNameAsync(newName);
if (existingAuthor != null && existingAuthor.Id != author.Id)
{
throw new AuthorAlreadyExistsException(newName);
}
author.ChangeName(newName);
}
}
}
````
* `AuthorManager` 强制使用一种可控的方式创建作者和修改作者的名字. 应用层 (后面会介绍) 将会使用这些方法.
> **DDD 提示**: 如非必须并且用于执行核心业务规则, 不要引入领域服务方法. 对于这个场景, 我们使用这个服务保证名字的唯一性.
两个方法都检查是否存在同名用户, 如果存在, 抛出业务异常 `AuthorAlreadyExistsException`, 这个异常定义在 `Acme.BookStore.Domain` 项目 (`Authors` 文件夹中):
````csharp
using Volo.Abp;
namespace Acme.BookStore.Authors
{
public class AuthorAlreadyExistsException : BusinessException
{
public AuthorAlreadyExistsException(string name)
: base(BookStoreDomainErrorCodes.AuthorAlreadyExists)
{
WithData("name", name);
}
}
}
````
`BusinessException` 是一个特殊的异常类型. 在需要时抛出领域相关异常是一个好的实践. ABP框架会自动处理它, 并且它也容易本地化. `WithData(...)` 方法提供额外的数据给异常对象, 这些数据将会在本地化中或出于其它一些目的被使用.
打开 `Acme.BookStore.Domain.Shared` 项目中的 `BookStoreDomainErrorCodes` 并修改为:
````csharp
namespace Acme.BookStore
{
public static class BookStoreDomainErrorCodes
{
public const string AuthorAlreadyExists = "BookStore:00001";
}
}
````
这里定义了一个字符串, 表示应用程序抛出的错误码, 这个错误码可以被客户端应用程序处理. 为了用户, 你可能希望本地化它. 打开 `Acme.BookStore.Domain.Shared` 项目中的 `Localization/BookStore/en.json` , 加入以下项:
````json
"BookStore:00001": "There is already an author with the same name: {name}"
````
`AuthorAlreadyExistsException` 被抛出, 终端用户将会在UI上看到组织好的错误消息.
## IAuthorRepository
`AuthorManager` 注入了 `IAuthorRepository`, 所以我们需要定义它. 在 `Acme.BookStore.Domain` 项目的 `Authors` 文件夹 (命名空间) 中创建这个新接口:
````csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Authors
{
public interface IAuthorRepository : IRepository<Author, Guid>
{
Task<Author> FindByNameAsync(string name);
Task<List<Author>> GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter = null
);
}
}
````
* `IAuthorRepository` 扩展了标准 `IRepository<Author, Guid>` 接口, 所以所有的标准 [repository](../Repositories.md) 方法对于 `IAuthorRepository` 都是可用的.
* `FindByNameAsync``AuthorManager` 中用来根据姓名查询用户.
* `GetListAsync` 用于应用层以获得一个排序的, 经过过滤的作者列表, 显示在UI上.
我们会在下一章实现这个repository.
> 这两个方法似乎 **看上去没有必要**, 因为标准repositories已经是 `IQueryable`, 你可以直接使用它们, 而不是自定义方法. 在实际应用程序中, 这么做是没问题的. 但在这个 **学习指南**中, 解释如何在需要时创建自定义repository方法是有价值的.
## 结论
这一章覆盖了图书管理程序作者相关功能的领域层. 在这一章中创建/更新的文件在下图中被高亮:
![bookstore-author-domain-layer](images/bookstore-author-domain-layer.png)
## 下一章
查看本教程的[下一章](Part-7.md).

239
docs/zh-Hans/Tutorials/Part-7.md

@ -1 +1,238 @@
TODO.. # Web应用程序开发教程 - 第七章: 数据库集成
````json
//[doc-params]
{
"UI": ["MVC","Blazor","BlazorServer","NG"],
"DB": ["EF","Mongo"]
}
````
## 关于本教程
在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:
* **{{DB_Text}}** 做为ORM提供程序.
* **{{UI_Value}}** 做为UI框架.
本教程分为以下部分:
- [Part 1: 创建服务端](Part-1.md)
- [Part 2: 图书列表页面](Part-2.md)
- [Part 3: 创建,更新和删除图书](Part-2.md)
- [Part 4: 集成测试](Part-4.md)
- [Part 5: 授权](Part-5.md)
- [Part 6: 作者: 领域层](Part-6.md)
- **Part 7: 数据库集成**(本章)
- [Part 8: 作者: 应用服务层](Part-8.md)
- [Part 9: 作者: 用户页面](Part-9.md)
- [Part 10: 图书到作者的关系](Part-10.md)
## 下载源码
本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合:
* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
> `git config --system core.longpaths true`
## 简介
这章阐述如何为前一章介绍的 `作者` 实体配置数据库集成.
{{if DB=="EF"}}
## DB Context
打开 `Acme.BookStore.EntityFrameworkCore` 项目中的 `BookStoreDbContext` 加入 `DbSet` 属性:
````csharp
public DbSet<Author> Authors { get; set; }
````
定位到相同项目中的 `BookStoreDbContext` 类中的 `OnModelCreating` 方法, 加入以下代码到方法的结尾:
````csharp
builder.Entity<Author>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Authors",
BookStoreConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Name)
.IsRequired()
.HasMaxLength(AuthorConsts.MaxNameLength);
b.HasIndex(x => x.Name);
});
````
这和前面的 `Book` 实体做的一样, 所以不再赘述.
## 创建数据库迁移
配置启动解决方案为使用 [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). 因为我们还没有修改数据库映射配置,所以需要创建一个新的迁移并对数据库应用变更.
打开命令行终端, 切换当前目录为 `Acme.BookStore.EntityFrameworkCore` 项目目录, 输入以下命令:
````bash
dotnet ef migrations add Added_Authors
````
这会在项目中添加一个迁移类:
![bookstore-efcore-migration-authors](./images/bookstore-efcore-migration-authors.png)
你可以在同一个命令行终端中使用以下命令对数据库应用更改:
````bash
dotnet ef database update
````
> 如果你使用 Visual Studio, 可能希望在 *Package Manager Console (PMC)* 使用 `Add-Migration Added_Authors -c BookStoreMigrationsDbContext``Update-Database -c BookStoreMigrationsDbContext` 命令. 如果这样, 保证 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且在PMC中 `Acme.BookStore.EntityFrameworkCore`*默认项目* .
{{else if DB=="Mongo"}}
## DB Context
打开 `Acme.BookStore.MongoDB` 项目 `MongoDb 文件夹`中的 `BookStoreMongoDbContext`, 在类中加入以下属性:
````csharp
public IMongoCollection<Author> Authors => Collection<Author>();
````
{{end}}
## 实现 IAuthorRepository
{{if DB=="EF"}}
`Acme.BookStore.EntityFrameworkCore` 项目 (`Authors` 文件夹)中创建一个新类 `EfCoreAuthorRepository`, 粘贴以下代码:
````csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading.Tasks;
using Acme.BookStore.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
namespace Acme.BookStore.Authors
{
public class EfCoreAuthorRepository
: EfCoreRepository<BookStoreDbContext, Author, Guid>,
IAuthorRepository
{
public EfCoreAuthorRepository(
IDbContextProvider<BookStoreDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<Author> FindByNameAsync(string name)
{
var dbSet = await GetDbSetAsync();
return await dbSet.FirstOrDefaultAsync(author => author.Name == name);
}
public async Task<List<Author>> GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter = null)
{
var dbSet = await GetDbSetAsync();
return await dbSet
.WhereIf(
!filter.IsNullOrWhiteSpace(),
author => author.Name.Contains(filter)
)
.OrderBy(sorting)
.Skip(skipCount)
.Take(maxResultCount)
.ToListAsync();
}
}
}
````
* 继承自 `EfCoreRepository`, 所以继承了标准repository的方法实现.
* `WhereIf` 是ABP 框架的快捷扩展方法. 它仅当第一个条件满足时, 执行 `Where` 查询. (根据名字查询, 仅当 filter 不为空). 你可以不使用这个方法, 但这些快捷方法可以提高效率.
* `sorting` 可以是一个字符串, 如 `Name`, `Name ASC``Name DESC`. 通过使用 [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) NuGet 包是可能的.
> 参阅 [EF Core 集成文档](../Entity-Framework-Core.md) 获得基于EF Core的repositories的更多信息.
{{else if DB=="Mongo"}}
`Acme.BookStore.MongoDB` 项目 (`Authors` 文件夹)中创建一个新类 `MongoDbAuthorRepository`, 粘贴以下代码:
```csharp
using System;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Collections.Generic;
using System.Threading.Tasks;
using Acme.BookStore.MongoDB;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Volo.Abp.Domain.Repositories.MongoDB;
using Volo.Abp.MongoDB;
namespace Acme.BookStore.Authors
{
public class MongoDbAuthorRepository
: MongoDbRepository<BookStoreMongoDbContext, Author, Guid>,
IAuthorRepository
{
public MongoDbAuthorRepository(
IMongoDbContextProvider<BookStoreMongoDbContext> dbContextProvider
) : base(dbContextProvider)
{
}
public async Task<Author> FindByNameAsync(string name)
{
var queryable = await GetMongoQueryableAsync();
return await queryable.FirstOrDefaultAsync(author => author.Name == name);
}
public async Task<List<Author>> GetListAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter = null)
{
var queryable = await GetMongoQueryableAsync();
return await queryable
.WhereIf<Author, IMongoQueryable<Author>>(
!filter.IsNullOrWhiteSpace(),
author => author.Name.Contains(filter)
)
.OrderBy(sorting)
.As<IMongoQueryable<Author>>()
.Skip(skipCount)
.Take(maxResultCount)
.ToListAsync();
}
}
}
```
* 继承自 `MongoDbRepository`, 所以继承了标准repository的方法实现.
* `WhereIf` 是ABP 框架的快捷扩展方法. 它仅当第一个条件满足时, 执行 `Where` 查询. (根据名字查询, 仅当 filter 不为空). 你可以不使用这个方法, 但这些快捷方法可以提高效率.
* `sorting` 可以是一个字符串, 如 `Name`, `Name ASC``Name DESC`. 通过使用 [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) NuGet 包是可能的.
> 参阅 [MongoDB 集成文档](../MongoDB.md) 获得基于MongoDB的repositories的更多信息.
{{end}}
## 下一章
查看本教程的[下一章](Part-8.md).

BIN
docs/zh-Hans/Tutorials/images/blazor-add-book-button.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
docs/zh-Hans/Tutorials/images/blazor-add-books-component.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/zh-Hans/Tutorials/images/blazor-bookstore-book-list.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
docs/zh-Hans/Tutorials/images/blazor-delete-book-action.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
docs/zh-Hans/Tutorials/images/blazor-edit-book-action-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
docs/zh-Hans/Tutorials/images/blazor-edit-book-modal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
docs/zh-Hans/Tutorials/images/blazor-menu-bookstore.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
docs/zh-Hans/Tutorials/images/blazor-new-book-modal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-efcore-migration-authors.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
docs/zh-Hans/Tutorials/images/bookstore-efcore-migration.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
docs/zh-Hans/Tutorials/images/generated-proxies-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/zh-Hans/Tutorials/images/vs-run-without-iisexpress.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

18
docs/zh-Hans/docs-nav.json

@ -5,11 +5,25 @@
"path": "Tutorials/Todo/Index.md" "path": "Tutorials/Todo/Index.md"
}, },
{ {
"text": "入门", "text": "入门教程",
"items": [ "items": [
{ {
"text": "Web应用程序", "text": "Web应用程序",
"path": "Getting-Started.md" "path": "Getting-Started.md",
"items": [
{
"text": "1: 设置你的开发环境",
"path": "Getting-Started-Setup-Environment.md"
},
{
"text": "2: 创建新的解决方案",
"path": "Getting-Started-Create-Solution.md"
},
{
"text": "3: 运行解决方案",
"path": "Getting-Started-Running-Solution.md"
}
]
}, },
{ {
"text": "控制台应用程序", "text": "控制台应用程序",

BIN
docs/zh-Hans/images/create-aspnet-core-application.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 41 KiB

BIN
docs/zh-Hans/images/identity-module-menu.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/zh-Hans/images/identity-module-permissions.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
docs/zh-Hans/images/identity-module-roles.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
docs/zh-Hans/images/identity-module-users.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

11
framework/src/Volo.Abp.ApiVersioning.Abstractions/Volo/Abp/ApiVersioning/AbpApiVersioningAbstractionsModule.cs

@ -1,13 +1,12 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity; using Volo.Abp.Modularity;
namespace Volo.Abp.ApiVersioning namespace Volo.Abp.ApiVersioning;
public class AbpApiVersioningAbstractionsModule : AbpModule
{ {
public class AbpApiVersioningAbstractionsModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context)
{ {
public override void ConfigureServices(ServiceConfigurationContext context) context.Services.AddSingleton<IRequestedApiVersion>(NullRequestedApiVersion.Instance);
{
context.Services.AddSingleton<IRequestedApiVersion>(NullRequestedApiVersion.Instance);
}
} }
} }

9
framework/src/Volo.Abp.ApiVersioning.Abstractions/Volo/Abp/ApiVersioning/IRequestedApiVersion.cs

@ -1,7 +1,6 @@
namespace Volo.Abp.ApiVersioning namespace Volo.Abp.ApiVersioning;
public interface IRequestedApiVersion
{ {
public interface IRequestedApiVersion string Current { get; }
{
string Current { get; }
}
} }

15
framework/src/Volo.Abp.ApiVersioning.Abstractions/Volo/Abp/ApiVersioning/NullRequestedApiVersion.cs

@ -1,14 +1,13 @@
namespace Volo.Abp.ApiVersioning namespace Volo.Abp.ApiVersioning;
public class NullRequestedApiVersion : IRequestedApiVersion
{ {
public class NullRequestedApiVersion : IRequestedApiVersion public static NullRequestedApiVersion Instance = new NullRequestedApiVersion();
{
public static NullRequestedApiVersion Instance = new NullRequestedApiVersion();
public string Current => null; public string Current => null;
private NullRequestedApiVersion() private NullRequestedApiVersion()
{ {
}
} }
} }

25
framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Microsoft/AspNetCore/Builder/ApplicationBuilderAbpJwtTokenMiddlewareExtension.cs

@ -1,25 +1,24 @@
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
namespace Microsoft.AspNetCore.Builder namespace Microsoft.AspNetCore.Builder;
public static class ApplicationBuilderAbpJwtTokenMiddlewareExtension
{ {
public static class ApplicationBuilderAbpJwtTokenMiddlewareExtension public static IApplicationBuilder UseJwtTokenMiddleware(this IApplicationBuilder app, string schema = JwtBearerDefaults.AuthenticationScheme)
{ {
public static IApplicationBuilder UseJwtTokenMiddleware(this IApplicationBuilder app, string schema = JwtBearerDefaults.AuthenticationScheme) return app.Use(async (ctx, next) =>
{ {
return app.Use(async (ctx, next) => if (ctx.User.Identity?.IsAuthenticated != true)
{ {
if (ctx.User.Identity?.IsAuthenticated != true) var result = await ctx.AuthenticateAsync(schema);
if (result.Succeeded && result.Principal != null)
{ {
var result = await ctx.AuthenticateAsync(schema); ctx.User = result.Principal;
if (result.Succeeded && result.Principal != null)
{
ctx.User = result.Principal;
}
} }
}
await next(); await next();
}); });
}
} }
} }

9
framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo/Abp/AspNetCore/Authentication/JwtBearer/AbpAspNetCoreAuthenticationJwtBearerModule.cs

@ -1,11 +1,10 @@
using Volo.Abp.Modularity; using Volo.Abp.Modularity;
using Volo.Abp.Security; using Volo.Abp.Security;
namespace Volo.Abp.AspNetCore.Authentication.JwtBearer namespace Volo.Abp.AspNetCore.Authentication.JwtBearer;
[DependsOn(typeof(AbpSecurityModule))]
public class AbpAspNetCoreAuthenticationJwtBearerModule : AbpModule
{ {
[DependsOn(typeof(AbpSecurityModule))]
public class AbpAspNetCoreAuthenticationJwtBearerModule : AbpModule
{
}
} }

83
framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Microsoft/AspNetCore/Authentication/OAuth/Claims/AbpClaimActionCollectionExtensions.cs

@ -1,57 +1,56 @@
using Volo.Abp.AspNetCore.Authentication.OAuth.Claims; using Volo.Abp.AspNetCore.Authentication.OAuth.Claims;
using Volo.Abp.Security.Claims; using Volo.Abp.Security.Claims;
namespace Microsoft.AspNetCore.Authentication.OAuth.Claims namespace Microsoft.AspNetCore.Authentication.OAuth.Claims;
public static class AbpClaimActionCollectionExtensions
{ {
public static class AbpClaimActionCollectionExtensions public static void MapAbpClaimTypes(this ClaimActionCollection claimActions)
{ {
public static void MapAbpClaimTypes(this ClaimActionCollection claimActions) if (AbpClaimTypes.UserName != "name")
{ {
if (AbpClaimTypes.UserName != "name") claimActions.MapJsonKey(AbpClaimTypes.UserName, "name");
{ claimActions.DeleteClaim("name");
claimActions.MapJsonKey(AbpClaimTypes.UserName, "name"); claimActions.RemoveDuplicate(AbpClaimTypes.UserName);
claimActions.DeleteClaim("name");
claimActions.RemoveDuplicate(AbpClaimTypes.UserName);
}
if (AbpClaimTypes.Email != "email")
{
claimActions.MapJsonKey(AbpClaimTypes.Email, "email");
claimActions.DeleteClaim("email");
claimActions.RemoveDuplicate(AbpClaimTypes.Email);
}
if (AbpClaimTypes.EmailVerified != "email_verified")
{
claimActions.MapJsonKey(AbpClaimTypes.EmailVerified, "email_verified");
}
if (AbpClaimTypes.PhoneNumber != "phone_number")
{
claimActions.MapJsonKey(AbpClaimTypes.PhoneNumber, "phone_number");
}
if (AbpClaimTypes.PhoneNumberVerified != "phone_number_verified")
{
claimActions.MapJsonKey(AbpClaimTypes.PhoneNumberVerified, "phone_number_verified");
}
if (AbpClaimTypes.Role != "role")
{
claimActions.MapJsonKeyMultiple(AbpClaimTypes.Role, "role");
}
claimActions.RemoveDuplicate(AbpClaimTypes.Name);
} }
public static void MapJsonKeyMultiple(this ClaimActionCollection claimActions, string claimType, string jsonKey) if (AbpClaimTypes.Email != "email")
{ {
claimActions.Add(new MultipleClaimAction(claimType, jsonKey)); claimActions.MapJsonKey(AbpClaimTypes.Email, "email");
claimActions.DeleteClaim("email");
claimActions.RemoveDuplicate(AbpClaimTypes.Email);
} }
public static void RemoveDuplicate(this ClaimActionCollection claimActions, string claimType) if (AbpClaimTypes.EmailVerified != "email_verified")
{ {
claimActions.Add(new RemoveDuplicateClaimAction(claimType)); claimActions.MapJsonKey(AbpClaimTypes.EmailVerified, "email_verified");
} }
if (AbpClaimTypes.PhoneNumber != "phone_number")
{
claimActions.MapJsonKey(AbpClaimTypes.PhoneNumber, "phone_number");
}
if (AbpClaimTypes.PhoneNumberVerified != "phone_number_verified")
{
claimActions.MapJsonKey(AbpClaimTypes.PhoneNumberVerified, "phone_number_verified");
}
if (AbpClaimTypes.Role != "role")
{
claimActions.MapJsonKeyMultiple(AbpClaimTypes.Role, "role");
}
claimActions.RemoveDuplicate(AbpClaimTypes.Name);
}
public static void MapJsonKeyMultiple(this ClaimActionCollection claimActions, string claimType, string jsonKey)
{
claimActions.Add(new MultipleClaimAction(claimType, jsonKey));
}
public static void RemoveDuplicate(this ClaimActionCollection claimActions, string claimType)
{
claimActions.Add(new RemoveDuplicateClaimAction(claimType));
} }
} }

9
framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Volo/Abp/AspNetCore/Authentication/OAuth/AbpAspNetCoreAuthenticationOAuthModule.cs

@ -1,11 +1,10 @@
using Volo.Abp.Modularity; using Volo.Abp.Modularity;
using Volo.Abp.Security; using Volo.Abp.Security;
namespace Volo.Abp.AspNetCore.Authentication.OAuth namespace Volo.Abp.AspNetCore.Authentication.OAuth;
[DependsOn(typeof(AbpSecurityModule))]
public class AbpAspNetCoreAuthenticationOAuthModule : AbpModule
{ {
[DependsOn(typeof(AbpSecurityModule))]
public class AbpAspNetCoreAuthenticationOAuthModule : AbpModule
{
}
} }

67
framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Volo/Abp/AspNetCore/Authentication/OAuth/Claims/MultipleClaimAction.cs

@ -3,51 +3,50 @@ using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Authentication.OAuth.Claims;
namespace Volo.Abp.AspNetCore.Authentication.OAuth.Claims namespace Volo.Abp.AspNetCore.Authentication.OAuth.Claims;
public class MultipleClaimAction : ClaimAction
{ {
public class MultipleClaimAction : ClaimAction public MultipleClaimAction(string claimType, string jsonKey)
: base(claimType, jsonKey)
{ {
public MultipleClaimAction(string claimType, string jsonKey)
: base(claimType, jsonKey)
{
} }
public override void Run(JsonElement userData, ClaimsIdentity identity, string issuer) public override void Run(JsonElement userData, ClaimsIdentity identity, string issuer)
{ {
JsonElement prop; JsonElement prop;
if (!userData.TryGetProperty(ValueType, out prop)) if (!userData.TryGetProperty(ValueType, out prop))
return; return;
if (prop.ValueKind == JsonValueKind.Null) if (prop.ValueKind == JsonValueKind.Null)
{ {
return; return;
} }
Claim claim; Claim claim;
switch (prop.ValueKind) switch (prop.ValueKind)
{ {
case JsonValueKind.String: case JsonValueKind.String:
claim = new Claim(ClaimType, prop.GetString(), ValueType, issuer); claim = new Claim(ClaimType, prop.GetString(), ValueType, issuer);
if (!identity.Claims.Any(c => c.Type == claim.Type && c.Value == claim.Value))
{
identity.AddClaim(claim);
}
break;
case JsonValueKind.Array:
foreach (var arramItem in prop.EnumerateArray())
{
claim = new Claim(ClaimType, arramItem.GetString(), ValueType, issuer);
if (!identity.Claims.Any(c => c.Type == claim.Type && c.Value == claim.Value)) if (!identity.Claims.Any(c => c.Type == claim.Type && c.Value == claim.Value))
{ {
identity.AddClaim(claim); identity.AddClaim(claim);
} }
break; }
case JsonValueKind.Array: break;
foreach (var arramItem in prop.EnumerateArray()) default:
{ throw new AbpException("Unhandled JsonValueKind: " + prop.ValueKind);
claim = new Claim(ClaimType, arramItem.GetString(), ValueType, issuer);
if (!identity.Claims.Any(c => c.Type == claim.Type && c.Value == claim.Value))
{
identity.AddClaim(claim);
}
}
break;
default:
throw new AbpException("Unhandled JsonValueKind: " + prop.ValueKind);
}
} }
} }
} }

39
framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Volo/Abp/AspNetCore/Authentication/OAuth/Claims/RemoveDuplicateClaimAction.cs

@ -5,35 +5,34 @@ using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Authentication.OAuth.Claims;
namespace Volo.Abp.AspNetCore.Authentication.OAuth.Claims namespace Volo.Abp.AspNetCore.Authentication.OAuth.Claims;
public class RemoveDuplicateClaimAction : ClaimAction
{ {
public class RemoveDuplicateClaimAction : ClaimAction public RemoveDuplicateClaimAction(string claimType)
: base(claimType, ClaimValueTypes.String)
{ {
public RemoveDuplicateClaimAction(string claimType) }
: base(claimType, ClaimValueTypes.String)
/// <inheritdoc />
public override void Run(JsonElement userData, ClaimsIdentity identity, string issuer)
{
var claims = identity.Claims.Where(c => c.Type == ClaimType).ToArray();
if (claims.Length < 2)
{ {
return;
} }
/// <inheritdoc /> var previousValues = new List<string>();
public override void Run(JsonElement userData, ClaimsIdentity identity, string issuer) foreach (var claim in claims)
{ {
var claims = identity.Claims.Where(c => c.Type == ClaimType).ToArray(); if (claim.Value.IsIn(previousValues))
if (claims.Length < 2)
{ {
return; identity.RemoveClaim(claim);
} }
else
var previousValues = new List<string>();
foreach (var claim in claims)
{ {
if (claim.Value.IsIn(previousValues)) previousValues.Add(claim.Value);
{
identity.RemoveClaim(claim);
}
else
{
previousValues.Add(claim.Value);
}
} }
} }
} }

81
framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Microsoft/Extensions/DependencyInjection/AbpOpenIdConnectExtensions.cs

@ -7,59 +7,58 @@ using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Volo.Abp.AspNetCore.MultiTenancy; using Volo.Abp.AspNetCore.MultiTenancy;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection;
public static class AbpOpenIdConnectExtensions
{ {
public static class AbpOpenIdConnectExtensions public static AuthenticationBuilder AddAbpOpenIdConnect(this AuthenticationBuilder builder)
{ => builder.AddAbpOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, _ => { });
public static AuthenticationBuilder AddAbpOpenIdConnect(this AuthenticationBuilder builder)
=> builder.AddAbpOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, _ => { });
public static AuthenticationBuilder AddAbpOpenIdConnect(this AuthenticationBuilder builder, Action<OpenIdConnectOptions> configureOptions) public static AuthenticationBuilder AddAbpOpenIdConnect(this AuthenticationBuilder builder, Action<OpenIdConnectOptions> configureOptions)
=> builder.AddAbpOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, configureOptions); => builder.AddAbpOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, configureOptions);
public static AuthenticationBuilder AddAbpOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, Action<OpenIdConnectOptions> configureOptions) public static AuthenticationBuilder AddAbpOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, Action<OpenIdConnectOptions> configureOptions)
=> builder.AddAbpOpenIdConnect(authenticationScheme, OpenIdConnectDefaults.DisplayName, configureOptions); => builder.AddAbpOpenIdConnect(authenticationScheme, OpenIdConnectDefaults.DisplayName, configureOptions);
public static AuthenticationBuilder AddAbpOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OpenIdConnectOptions> configureOptions) public static AuthenticationBuilder AddAbpOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OpenIdConnectOptions> configureOptions)
{
return builder.AddOpenIdConnect(authenticationScheme, displayName, options =>
{ {
return builder.AddOpenIdConnect(authenticationScheme, displayName, options => options.ClaimActions.MapAbpClaimTypes();
{
options.ClaimActions.MapAbpClaimTypes();
configureOptions?.Invoke(options); configureOptions?.Invoke(options);
options.Events ??= new OpenIdConnectEvents(); options.Events ??= new OpenIdConnectEvents();
var authorizationCodeReceived = options.Events.OnAuthorizationCodeReceived ?? (_ => Task.CompletedTask); var authorizationCodeReceived = options.Events.OnAuthorizationCodeReceived ?? (_ => Task.CompletedTask);
options.Events.OnAuthorizationCodeReceived = receivedContext => options.Events.OnAuthorizationCodeReceived = receivedContext =>
{ {
SetAbpTenantId(receivedContext); SetAbpTenantId(receivedContext);
return authorizationCodeReceived.Invoke(receivedContext); return authorizationCodeReceived.Invoke(receivedContext);
}; };
options.Events.OnRemoteFailure = remoteFailureContext => options.Events.OnRemoteFailure = remoteFailureContext =>
{
if (remoteFailureContext.Failure is OpenIdConnectProtocolException &&
remoteFailureContext.Failure.Message.Contains("access_denied"))
{ {
if (remoteFailureContext.Failure is OpenIdConnectProtocolException && remoteFailureContext.HandleResponse();
remoteFailureContext.Failure.Message.Contains("access_denied")) remoteFailureContext.Response.Redirect($"{remoteFailureContext.Request.PathBase}/");
{ }
remoteFailureContext.HandleResponse(); return Task.CompletedTask;
remoteFailureContext.Response.Redirect($"{remoteFailureContext.Request.PathBase}/"); };
} });
return Task.CompletedTask; }
};
});
}
private static void SetAbpTenantId(AuthorizationCodeReceivedContext receivedContext) private static void SetAbpTenantId(AuthorizationCodeReceivedContext receivedContext)
{ {
var tenantKey = receivedContext.HttpContext.RequestServices var tenantKey = receivedContext.HttpContext.RequestServices
.GetRequiredService<IOptions<AbpAspNetCoreMultiTenancyOptions>>().Value.TenantKey; .GetRequiredService<IOptions<AbpAspNetCoreMultiTenancyOptions>>().Value.TenantKey;
if (receivedContext.Request.Cookies.ContainsKey(tenantKey)) if (receivedContext.Request.Cookies.ContainsKey(tenantKey))
{ {
receivedContext.TokenEndpointRequest.SetParameter(tenantKey, receivedContext.TokenEndpointRequest.SetParameter(tenantKey,
receivedContext.Request.Cookies[tenantKey]); receivedContext.Request.Cookies[tenantKey]);
}
} }
} }
} }

13
framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Volo/Abp/AspNetCore/Authentication/OpenIdConnect/AbpAspNetCoreAuthenticationOpenIdConnectModule.cs

@ -2,13 +2,12 @@
using Volo.Abp.Modularity; using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
namespace Volo.Abp.AspNetCore.Authentication.OpenIdConnect namespace Volo.Abp.AspNetCore.Authentication.OpenIdConnect;
[DependsOn(
typeof(AbpMultiTenancyModule),
typeof(AbpAspNetCoreAuthenticationOAuthModule))]
public class AbpAspNetCoreAuthenticationOpenIdConnectModule : AbpModule
{ {
[DependsOn(
typeof(AbpMultiTenancyModule),
typeof(AbpAspNetCoreAuthenticationOAuthModule))]
public class AbpAspNetCoreAuthenticationOpenIdConnectModule : AbpModule
{
}
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save