pull/11231/head
Alper Ebicoglu 4 years ago
commit 11b5aec688

@ -41,12 +41,12 @@ jobs:
dotnet-version: 6.0.100
- name: Build All
run: .\build-all.ps1
run: .\build-all.ps1 -f
working-directory: .\build
shell: powershell
- name: Test All
run: .\test-all.ps1
run: .\test-all.ps1 -f
working-directory: .\build
shell: powershell

@ -155,82 +155,82 @@
"WeAreHereToHelp": "We are Here to <span class=\"zero-text\">Help</span>",
"BrowseOrAskQuestion": "You can browse our help topics or search in frequently asked questions, or you can ask us a question by using the <a href=\"{0}\" class=\"text-success\">contact form</a>.",
"SearchQuestionPlaceholder": "Search in frequently asked questions",
"WhatIsTheABPCommercial": "What is the ABP Commercial?",
"WhatAreDifferencesThanAbpFramework": "What are the differences between the open source ABP Framework and the ABP Commercial?",
"ABPCommercialExplanation": "ABP Commercial is a set of premium modules, tools, themes and services built on top of the open source <a target=\"_blank\" href=\"{0}\">ABP framework</a>. ABP Commercial is being developed and supported by the same team behind the ABP framework.",
"WhatAreDifferencesThanABPFrameworkExplanation": "<p> <a target=\"_blank\" href=\"{0}\">ABP framework</a> is a modular, themeable, micro-service compatible application development framework for ASP.NET Core. It provides a complete architecture and a strong infrastructure to make you focusing on your own business code rather than repeating yourself for every new project. It is based on software development best practices and popular tools you already know. </p> <p> ABP framework is completely free, open source and community-driven. It also provides a free theme and some pre-built modules (e.g. identity management and tenant management).</p>",
"VisitTheFrameworkVSCommercialDocument": "Visit the following link, for more information <a href=\"{0}\" target=\"_blank\"> {1} </a>",
"ABPCommercialFollowingBenefits": "ABP Commercial adds the following benefits on top of the ABP framework;",
"WhatIsTheABPCommercial": "What is ABP Commercial?",
"WhatAreDifferencesThanAbpFramework": "What are the differences between the open source ABP Framework and ABP Commercial?",
"ABPCommercialExplanation": "ABP Commercial is a set of premium modules, tools, themes and services that are built on top of the open source <a target=\"_blank\" href=\"{0}\">ABP framework</a>. ABP Commercial is being developed and supported by the same team behind the ABP framework.",
"WhatAreDifferencesThanABPFrameworkExplanation": "<p> <a target=\"_blank\" href=\"{0}\">ABP framework</a> is a modular, themeable, micro-service compatible application development framework for ASP.NET Core. It provides a complete architecture and a strong infrastructure to let you focus on your own business code rather than repeating yourself for every new project. It is based on the best practices of software development and popular tools you already know. </p> <p> ABP framework is completely free, open source and community-driven. It also provides a free theme and some pre-built modules (e.g. identity management and tenant management).</p>",
"VisitTheFrameworkVSCommercialDocument": "Visit the following link for more information <a href=\"{0}\" target=\"_blank\"> {1} </a>",
"ABPCommercialFollowingBenefits": "ABP Commercial adds the following benefits on top of the ABP framework:",
"Professional": "Professional",
"UIThemes": "UI themes",
"EnterpriseModules": "Enterprise ready, feature rich, pre-built <a href=\"{0}\">application modules</a> (e.g. Identity Server management, SaaS management, language management)",
"UIThemes": "UI Themes",
"EnterpriseModules": "Enterprise ready, feature rich, pre-built <a href=\"{0}\">Application Modules</a> (e.g. Identity Server management, SaaS management, language management)",
"ToolingToSupport": "Tooling to support your development productivity (e.g. <a href=\"{0}\">ABP Suite</a>)",
"PremiumSupportLink": "Premium <a href=\"{0}\" target=\"_blank\">support</a>",
"PremiumSupportLink": "Premium <a href=\"{0}\" target=\"_blank\">Support</a>",
"WhatDoIDownloadABPCommercial": "What do I download when I purchase the ABP Commercial?",
"CreateUnlimitedSolutions": "Once you purchase an ABP Commercial license, you will be able to create unlimited solutions like described in the <a href=\"{0}\">Getting Started</a> document.",
"ABPCommercialSolutionExplanation": "When you create a new application, you get a Visual Studio solution (a startup template) based on your preferences. The downloaded solution has commercial modules and themes already installed and configured for you. You can remove a pre-installed module or add another module if you like. All modules and themes are used a NuGet/NPM packages by default.",
"StartDevelopWithTutorials": "The downloaded solution is well architected and documented. You can start to develop your own business code based on it following <a href=\"{0}\">the tutorials</a>",
"TryTheCommercialDemo": "You can try <a href=\"{0}\">the demo</a> to see a sample application created using the ABP Commercial startup template.",
"ABPCommercialSolutionExplanation": "When you create a new application, you get a Visual Studio solution (a startup template) based on your preferences. The downloaded solution has commercial modules and themes already installed and configured for you. You can remove a pre-installed module or add another module if you like. All modules and themes use NuGet/NPM packages by default.",
"StartDevelopWithTutorials": "The downloaded solution is well architected and documented. You can start developing your own business code based on it following the <a href=\"{0}\">tutorials</a>.",
"TryTheCommercialDemo": "You can try the <a href=\"{0}\">Live Demo</a> to see a sample application created using the ABP Commercial startup template.",
"HowManyProducts": "How many different products/solutions can I build using the ABP Commercial?",
"HowManyProductsExplanation": "You can create as many projects as you want during your active license period, there is no limit! After your license expires, you cannot create new projects, but you can continue to develop the projects you have downloaded and deploy them to an unlimited count of servers.",
"HowManyDevelopers": "How many developers can work on the ABP Commercial?",
"HowManyDevelopersExplanation": "ABP Commercial licenses are per developer. Different license types have different developer limits. However, you can add more developers to any license type whenever you need. See <a href=\"{0}\">the prices</a> page for license types, developer limits and additional developer costs.",
"HowManyDevelopersExplanation": "ABP Commercial licenses are per developer. Different license types have different developer limits. However, you can add more developers to any license type whenever you need. Check out the <a href=\"{0}\">Plans & Pricing</a> page for license types, developer limits and additional developer costs.",
"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, check out the \"How many developers can work on the ABP Commercial?\" FAQ.",
"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. 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.",
"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. Check out the <a href=\"/pricing\">license comparison table</a> to see 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?",
"LicenseRenewalCostExplanation": "The renewal (extend) price of the standard Team License is ${0}, standard Business License is ${1} and standard Enterprise License is ${2}. If you are already a customer, <a href='{3}' target='_blank'>log into your account</a> to review the available renewal pricing.",
"LicenseRenewalCostExplanation": "The renewal (extend) price of the standard Team License is ${0}, standard Business License is ${1} and standard Enterprise License is ${2}. If you are already a customer, <a href='{3}' target='_blank'>log into your account</a> to review the current renewal pricing.",
"HowDoIRenewMyLicense": "How do I renew my license?",
"HowDoIRenewMyLicenseExplanation": "You can renew your license by navigating to the <a href='{0}' target='_blank'>organization management page</a>. In order to take advantage of our discounted Early Renewal rates, make sure you renew before your license expires. Don't worry about not knowing when your Early Renewal opportunity closes, however. You'll receive 3 reminder e-mails before your subscription expires. We'll send them at 30 days, 7 days and 1 day before expiration.",
"HowDoIRenewMyLicenseExplanation": "You can renew your license by navigating to the <a href='{0}' target='_blank'>organization management page</a>. In order to take advantage of our discounted Early Renewal rates, make sure you renew before your license expires. Don't worry about not knowing when your Early Renewal opportunity closes, you'll however receive 3 reminder e-mails before your subscription expires. We'll send them 30 days, 7 days and 1 day before expiration.",
"IsSourceCodeIncluded": "Does my license include the source code of the commercial modules and themes?",
"IsSourceCodeIncludedExplanation1": "Depends on the license type you've purchased:",
"IsSourceCodeIncludedExplanation2": "<strong>Team</strong>: Your solution uses the modules and the themes as NuGet and NPM packages. It doesn't include their source code. In this way, you can easily upgrade these modules and themes whenever a new version is available. However, you can not get the source code of the modules and the themes.",
"IsSourceCodeIncludedExplanation2": "<strong>Team</strong>: Your solution uses the modules and themes as NuGet and NPM packages. It doesn't include their source code. In this way, you can easily upgrade these modules and themes whenever a new version is available. However, you can not get the source code of these modules and themes.",
"IsSourceCodeIncludedExplanation3": "<strong>Business/Enterprise</strong>: In addition to the Team license, you are able to download the source code of any module or theme you need. You can even remove the NuGet/NPM package references for a particular module and add its source code directly to your solution to fully change it.",
"IsSourceCodeIncludedExplanation4": "<p>Including the source code of a module to your solution gives you the maximum freedom to customize that module. However, then it will not be possible to automatically upgrade the module when a new version is released.</p><p>None of the licenses include the ABP Suite source code, which is an external tool that generates code for you and assist to your development.</p><p>See <a href=\"{0}\">the pricing</a> page for other differences between the license types.</p>",
"IsSourceCodeIncludedExplanation4": "<p>Including the source code of a module to your solution gives you the maximum freedom to customize that module. However, it will then not be possible to automatically upgrade the module when a new version is released.</p><p>None of the licenses include the ABP Suite source code, which is an external tool that generates code for you and assists your development.</p><p>Check out the <a href=\"{0}\">Plans & Pricing</a> page for other differences between the license types.</p>",
"ChangingDevelopers": "Can I change the registered developers of my organization in the future?",
"ChangingDevelopersExplanation": "In addition to add new developers to your license, you can also change the existing developers (you can remove a developer and add a new one to the same seat) without any additional cost.",
"ChangingDevelopersExplanation": "In addition to adding new developers to your license, you can also change the existing developers (you can remove a developer and add a new one to the same seat) without any additional cost.",
"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 update and support plan out of the box. To continue to get new features, performance enhancements, bug fixes, support and continue to use ABP Suite, you need to renew your license. When your license expires, you will not get the following benefits;",
"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 (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). For example, when your license expired, the latest release was v4.4.3 and the latest preview version was v5.0.0-rc.2, you can access the v4.X.X but you cannot access the v5.X.X.",
"WhatHappensWhenLicenseEndsExplanation1": "The ABP Commercial license is a <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 update and support plan out of the box. In order to continue to get new features, performance enhancements, bug fixes, support and continue using ABP Suite, you need to renew your license. When your license expires, you will not get the following benefits:",
"WhatHappensWhenLicenseEndsExplanation2": "You can not create new solutions using the ABP Commercial, but you can continue developing your existing applications forever.",
"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). For example, when your license expired, the latest release was v4.4.3 and the latest preview version was v5.0.0-rc.2, you would be able to access the v4.X.X but you wouldn't be access the v5.X.X.",
"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.",
"WhatHappensWhenLicenseEndsExplanation6": "You can not get the <a href=\"{0}\">premium support</a> anymore.",
"WhatHappensWhenLicenseEndsExplanation7": "You can extend (renew) your license if you want to continue to get these benefits. If you extend your license within <strong>1 month</strong> after your license expires, the following discounts will be applied: Team License {0}% discount, Business License {1}% discount, Enterprise License {2}% discount.",
"WhatHappensWhenLicenseEndsExplanation7": "You can extend (renew) your license if you want to continue getting these benefits. If you extend your license within <strong>1 month</strong> after your license expires, the following discounts will be applied: Team License {0}% discount, Business License {1}% discount, Enterprise License {2}% discount.",
"WhenShouldIRenewMyLicense": "When should I renew my license?",
"WhenShouldIRenewMyLicenseExplanation": "If you renew your license within <strong>1 month</strong> after your license expires, the following discounts will be applied: Team License {0}% discount, Business License {1}% discount, Enterprise License {2}% discount. If you renew your license <strong>1 month</strong> after the expiry date of your license, the renewal price will be the same as the license purchase price and there will be no discount on your renewal.",
"WhenShouldIRenewMyLicenseExplanation": "If you renew your license within <strong>1 month</strong> after your license expires, the following discounts will be applied: Team License {0}% discount, Business License {1}% discount, Enterprise License {2}% discount. However, if you renew your license after <strong>1 month</strong> since the expiry date of your license, the renewal price will be the same as the license purchase price and there will be no discount on your renewal.",
"TrialPlan": "Do you have a trial plan?",
"TrialPlanExplanation": "For now, ABP Commercial doesn't have a trial plan. For the Team licenses we provide 30 days money back guarantee. You can just request a refund in the first 30 days. For the Business and Enterprise licenses, we provide 60% refund in 30 days. This is because Business and Enterprise licenses include the full source code of all the modules and the themes.",
"DoYouAcceptBankWireTransfer": "Do you accept bank wire transfer?",
"DoYouAcceptBankWireTransferExplanation": "Yes, we accept bank wire transfer.<br />After sending the license fee via bank transfer, email us at accounting@abp.io your receipt and the type of license requested. Our international bank account information:",
"DoYouAcceptBankWireTransfer": "Do you accept bank wire transfers?",
"DoYouAcceptBankWireTransferExplanation": "Yes, we accept bank wire transfers.<br />After sending the license fee via bank transfer, email us your receipt and the type of license requested at accounting@abp.io, here's our international bank account information:",
"HowToUpgrade": "How to upgrade existing applications when a new version is available?",
"HowToUpgradeExplanation1": "When you create a new application using ABP Commercial, all the modules and the theme are used as NuGet and NPM packages. So, you can easily upgrade the packages when a new version is available.",
"HowToUpgradeExplanation1": "When you create a new application using ABP Commercial, all the modules and theme are used as NuGet and NPM packages. So, you can easily upgrade the packages when a new version is available.",
"HowToUpgradeExplanation2": "In addition to the standard NuGet/NPM upgrades, <a href=\"{0}\">ABP CLI</a> provides an update command that automatically finds and upgrades all ABP related packages in your solution.",
"DatabaseSupport": "Which database systems are supported?",
"DatabaseSupportExplanation": "ABP Framework itself is database agnostic and can work with any database provider by its nature. See <a href=\"{0}\" target=\"_blank\">the data access document</a> for a list of currently implemented providers.",
"DatabaseSupportExplanation": "ABP Framework itself is database agnostic and can work with any database provider by its nature. Check out the <a href=\"{0}\" target=\"_blank\">Data Access document</a> for a list of currently implemented providers.",
"UISupport": "Which UI frameworks are supported?",
"Supported": "Supported",
"UISupportExplanation": "ABP Framework itself is UI framework agnostic and can work with any UI framework. However, startup templates, module UIs and themes were not implemented for all UI frameworks. See <a href=\"{0}\">the getting started document</a> for the up-to-date list of UI options.",
"UISupportExplanation": "ABP Framework itself is UI framework agnostic and can work with any UI framework. However, startup templates, module UIs and themes were not implemented for all UI frameworks. Check out the <a href=\"{0}\">Getting Started document</a> for the up-to-date list of UI options.",
"MicroserviceSupport": "Does it support the micro-service architecture?",
"MicroserviceSupportExplanation1": "One of the major goals of the ABP framework is to provide a convenient infrastructure to create micro-service solutions. See the <a href=\"{0}\">micro-service architecture</a> document to understand how it helps to create micro-service systems.",
"MicroserviceSupportExplanation2": "All the ABP Commercial modules are designed to support micro-service deployment scenarios (with its own API and database) by following the <a href=\"{0}\">module development best practices</a> document.",
"MicroserviceSupportExplanation3": "We provide a sample <a href=\"{0}\">micro-service demo solution</a>that demonstrates a micro-service architecture implementation to help you to create your own solution.",
"MicroserviceSupportExplanation4": "So, the short answer is \"<strong>yes, it supports micro-service architecture</strong>\".",
"MicroserviceSupportExplanation1": "One of the major goals of the ABP framework is to provide a convenient infrastructure to create micro-service solutions. Check out the <a href=\"{0}\">Micro-service Architecture document</a> to understand how it helps with creating micro-service systems.",
"MicroserviceSupportExplanation2": "All the ABP Commercial modules are designed to support micro-service deployment scenarios (with its own API and database) by following the <a href=\"{0}\">Module Development Best Practices document</a>.",
"MicroserviceSupportExplanation3": "We provide a sample <a href=\"{0}\">Micro-service Demo Solution</a> that demonstrates a micro-service architecture implementation to help you create your own solution.",
"MicroserviceSupportExplanation4": "So, the short answer is: \"<strong>Yes, it supports micro-service architecture</strong>\".",
"MicroserviceSupportExplanation5": "However, a micro-service system is a solution and every solution will have different requirements, network topology, communication scenarios, authentication possibilities, database separation/sharing decisions, runtime configurations, 3rd party system integrations and many more.",
"MicroserviceSupportExplanation6": "The ABP Framework and the ABP Commercial provides infrastructure for micro-service scenarios, micro-service compatible modules, samples and documentation to help you to build your own solution. But don't expect to directly download your dream solution pre-built for you. You will need to understand it and bring some parts together based on your requirements.",
"WhereCanIDownloadSourceCode": "Where can I download source-code?",
"WhereCanIDownloadSourceCodeExplanation": "You can download the source code of all ABP modules, Angular packages and themes via ABP Suite or ABP CLI. See <a href=\"{0}\">How to download source-code?</a>",
"MicroserviceSupportExplanation6": "The ABP Framework and ABP Commercial provide infrastructure for micro-service scenarios, micro-service compatible modules, samples and documentation to help you build your own solution. But don't expect to directly download your dream solution pre-built for you. You will need to understand it and bring specific parts together based on your requirements.",
"WhereCanIDownloadSourceCode": "Where can I download the source-code?",
"WhereCanIDownloadSourceCodeExplanation": "You can download the source code of all the ABP modules, Angular packages and themes via ABP Suite or ABP CLI. Check out <a href=\"{0}\">How to download the source-code?</a>",
"ComputerLimitation": "How many computers can a developer login when developing ABP?",
"ComputerLimitationExplanation": "We specifically permit <strong>{0} computers</strong> per individual/licensed developer. Whenever there is a need for a developer to develop ABP Commercial products on a third machine, an e-mail should be sent to license@abp.io explaining the situation and we will then make the appropriate allocation in our system.",
"RefundPolicy": "Do you have a refund policy?",
"RefundPolicyExplanation": "You can request a refund within <strong>30 days</strong> of your license purchase. The Business and Enterprise license types have source-code download option, therefore refunds are not available for the Business and Enterprise (and any licenses that include a right to receive source-code). In addition, no refunds are made for renewals and second license purchases.",
"RefundPolicyExplanation": "You can request a refund within <strong>30 days</strong> of your license purchase. The Business and Enterprise license types have source-code download option, therefore refunds are not available for the Business and Enterprise (along with any licenses that include a right to receive source-code). In addition, no refunds are made for renewals and second license purchases.",
"HowCanIRefundVat": "How can I refund VAT?",
"HowCanIRefundVatExplanation1": "If you made the payment using 2Checkout, you can refund VAT via your 2Checkout account:",
"HowCanIRefundVatExplanation2": "Log in into your <a href=\"https://secure.2checkout.com/cpanel/login.php\" target=\"_blank\">2Checkout</a> account",
"HowCanIRefundVatExplanation2": "Log in to your <a href=\"https://secure.2checkout.com/cpanel/login.php\" target=\"_blank\">2Checkout</a> account",
"HowCanIRefundVatExplanation3": "Find the appropriate order and press \"Refund Belated VAT\" (enter your VAT ID)",
"HowCanIGetMyInvoice": "How can I get my invoice?",
"HowCanIGetMyInvoiceExplanation": "There are 2 payment gateways for purchasing a license: Iyzico and 2Checkout. If you purchase your license through the 2Checkout gateway, it sends the PDF invoice to your email address, see <a href=\"https://knowledgecenter.2checkout.com/Documentation/03Billing-and-payments/Payment-operations/How-do-invoices-work\">2Checkout invoicing.</a> If you purchase through the Iyzico gateway, with custom purchase link or via bank wire transfer, we will prepare and send your invoice. You can request or download your invoice from the <a href=\"{0}\">organization management page</a>. Before contacting us for the invoice, check your organization management page!",
"HowCanIGetMyInvoiceExplanation": "There are 2 payment gateways for purchasing a license: Iyzico and 2Checkout. If you purchase your license through the 2Checkout gateway, it sends the PDF invoice to your email address, check out <a href=\"https://knowledgecenter.2checkout.com/Documentation/03Billing-and-payments/Payment-operations/How-do-invoices-work\">2Checkout invoicing.</a> If you purchase through the Iyzico gateway, with a custom purchase link or via a bank wire transfer, we will prepare and send your invoice. You can request or download your invoice from the <a href=\"{0}\">organization management page</a>. Before contacting us for the invoice, check your organization management page!",
"Forum": "Forum",
"SupportExplanation": "ABP Commercial licenses provides a premium forum support by a team consists of the ABP Framework experts.",
"PrivateTicket": "Private Ticket",
@ -289,7 +289,7 @@
"PrivateTicketEmailSupport": "Private ticket & email support",
"BuyNow": "Buy Now",
"PayViaAmexCard": "How can I pay via my AMEX card?",
"PayViaAmexCardDescription": "The default payment gateway 'Iyzico' may decline some AMEX credit cards due to the security measures. In this case, you can pay through the alternative payment gateway '2Checkout'.",
"PayViaAmexCardDescription": "The default payment gateway 'Iyzico' may decline some AMEX credit cards due to security measures. In this case, you can pay through the alternative payment gateway '2Checkout'.",
"ThankYou": "Thank you",
"InvalidReCaptchaErrorMessage": "There was an error verifying reCAPTCHA. Please try again.",
"CompanyName": "Company name",
@ -391,7 +391,7 @@
"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>.",
"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> `.",
"DowngradeLicensePlanExplanation": "You cannot downgrade your existing license plan. But you can purchase a new lower license plan and continue 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.",

@ -66,6 +66,33 @@ public class MyLogWorker : HangfireBackgroundWorkerBase
> You can directly implement the `IHangfireBackgroundWorker`, but `HangfireBackgroundWorkerBase` provides some useful properties like Logger.
### UnitOfWork
For use with `UnitOfWorkAttribute`, you need to define an interface for worker:
```csharp
public interface IMyLogWorker : IHangfireBackgroundWorker
{
}
[ExposeServices(typeof(IMyLogWorker))]
public class MyLogWorker : HangfireBackgroundWorkerBase, IMyLogWorker
{
public MyLogWorker()
{
RecurringJobId = nameof(MyLogWorker);
CronExpression = Cron.Daily();
}
[UnitOfWork]
public override Task DoWorkAsync()
{
Logger.LogInformation("Executed MyLogWorker..!");
return Task.CompletedTask;
}
}
```
## Register BackgroundWorkerManager
After creating a background worker class, you should add it to the `IBackgroundWorkerManager`. The most common place is the `OnApplicationInitialization` method of your module class:
@ -78,6 +105,9 @@ public class MyModule : AbpModule
ApplicationInitializationContext context)
{
context.AddBackgroundWorker<MyLogWorker>();
//If the interface is defined
//context.AddBackgroundWorker<IMyLogWorker>();
}
}
````

@ -19,15 +19,14 @@ Click to a feature to understand and learn how to use it.
All features are individually usable. If you disable a feature, it completely disappears from your application, even from the database tables, by the help of the [Global Features](../../Global-Features.md) system.
## Pre Requirements
- This module depends on [BlobStoring](../../Blob-Storing.md) module for keeping media content.
> Make sure `BlobStoring` module is installed and at leats one provider is configured properly. For more information, check the [documentation](../../Blob-Storing.md).
CMS Kit uses [distributed cache](../../Caching.md) for responding faster.
- CMS Kit uses [distributed cache](../../Caching.md) for responding faster.
> Using a distributed cache, such as [Redis](../../Redis-Cache.md), is highly recommended for data consistency in distributed/clustered deployments.
## How to Install
> This module is depends on [BlobStoring](../../Blob-Storing.md) module, please install `BlobStoring` module first and add a provider. For more information, check the [documentation](../../Blob-Storing.md).
[ABP CLI](../../CLI.md) allows installing a module to a solution using the `add-module` command. You can install the CMS Kit module in a command-line terminal with the following command:
```bash
@ -77,4 +76,4 @@ All tables/collections use the `Cms` prefix by default. Set static properties on
This module uses `CmsKit` for the connection string name. If you don't define a connection string with this name, it fallbacks to the `Default` connection string.
See the [connection strings](https://docs.abp.io/en/abp/latest/Connection-Strings) documentation for details.
See the [connection strings](https://docs.abp.io/en/abp/latest/Connection-Strings) documentation for details.

@ -13,7 +13,7 @@ In this tutorial series, you will build an ABP based web application named `Acme
* **{{DB_Value}}** as the ORM provider.
* **{{UI_Value}}** as the UI Framework.
This tutorial is organized as the following parts;
This tutorial is organized as the following parts:
- [Part 1: Creating the server side](Part-1.md)
- [Part 2: The book list page](Part-2.md)
@ -51,19 +51,19 @@ This part is also recorded as a video tutorial and **<a href="https://www.youtub
## Creating a New Book
In this section, you will learn how to create a new modal dialog form to create a new book. The modal dialog will look like in the image below:
In this section, you will learn how to create a new modal dialog form to create a new book. The modal dialog will look like the image below:
![bookstore-create-dialog](images/bookstore-create-dialog-2.png)
### Create the Modal Form
Create a new razor page, named `CreateModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project.
Create a new razor page named `CreateModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project.
![bookstore-add-create-dialog](images/bookstore-add-create-dialog-v2.png)
#### CreateModal.cshtml.cs
Open the `CreateModal.cshtml.cs` file (`CreateModalModel` class) and replace with the following code:
Open the `CreateModal.cshtml.cs` file (`CreateModalModel` class) and replace it with the following code:
````C#
using System.Threading.Tasks;
@ -98,7 +98,7 @@ namespace Acme.BookStore.Web.Pages.Books
}
````
* This class is derived from the `BookStorePageModel` instead of standard `PageModel`. `BookStorePageModel` indirectly inherits the `PageModel` and adds some common properties & methods that can be shared in your page model classes.
* This class is derived from the `BookStorePageModel` instead of the standard `PageModel`. `BookStorePageModel` indirectly inherits the `PageModel` and adds some common properties & methods that can be shared in your page model classes.
* `[BindProperty]` attribute on the `Book` property binds post request data to this property.
* This class simply injects the `IBookAppService` in the constructor and calls the `CreateAsync` method in the `OnPostAsync` handler.
* It creates a new `CreateUpdateBookDto` object in the `OnGet` method. ASP.NET Core can work without creating a new instance like that. However, it doesn't create an instance for you and if your class has some default value assignments or code execution in the class constructor, they won't work. For this case, we set default values for some of the `CreateUpdateBookDto` properties.
@ -129,7 +129,7 @@ Open the `CreateModal.cshtml` file and paste the code below:
</abp-dynamic-form>
````
* This modal uses `abp-dynamic-form` [tag helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) to automatically create the form from the `CreateBookViewModel` model class.
* This modal uses `abp-dynamic-form` [tag helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) to automatically create the form from the `CreateBookViewModel` model class.
* `abp-model` attribute indicates the model object where it's the `Book` property in this case.
* `abp-form-content` tag helper is a placeholder to render the form controls (it is optional and needed only if you have added some other content in the `abp-dynamic-form` tag, just like in this page).
@ -155,7 +155,7 @@ Open the `Pages/Books/Index.cshtml` and set the content of `abp-card-header` tag
</abp-card-header>
````
The final content of the `Index.cshtml` is shown below:
The final content of `Index.cshtml` is shown below:
````html
@page
@ -193,7 +193,7 @@ This adds a new button called **New book** to the **top-right** of the table:
![bookstore-new-book-button](images/bookstore-new-book-button-2.png)
Open the `Pages/Books/Index.js` and add the following code just after the `Datatable` configuration:
Open the `Pages/Books/Index.js` file and add the following code right after the `Datatable` configuration:
````js
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
@ -208,11 +208,11 @@ $('#NewBookButton').click(function (e) {
});
````
* `abp.ModalManager` is a helper class to manage modals in the client side. It internally uses Twitter Bootstrap's standard modal, but abstracts many details by providing a simple API.
* `abp.ModalManager` is a helper class to manage modals on the client side. It internally uses Twitter Bootstrap's standard modal, but abstracts many details by providing a simple API.
* `createModal.onResult(...)` used to refresh the data table after creating a new book.
* `createModal.open();` is used to open the model to create a new book.
The final content of the `Index.js` should be like that:
The final content of the `Index.js` file should be like this:
````js
$(function () {
@ -290,7 +290,7 @@ Create a new razor page, named `EditModal.cshtml` under the `Pages/Books` folder
### EditModal.cshtml.cs
Open the `EditModal.cshtml.cs` file (`EditModalModel` class) and replace with the following code:
Open the `EditModal.cshtml.cs` file (`EditModalModel` class) and replace it with the following code:
````csharp
using System;
@ -331,13 +331,13 @@ namespace Acme.BookStore.Web.Pages.Books
}
````
* `[HiddenInput]` and `[BindProperty]` are standard ASP.NET Core MVC attributes. `SupportsGet` is used to be able to get `Id` value from query string parameter of the request.
* In the `OnGetAsync` method, we get `BookDto ` from the `BookAppService` and this is being mapped to the DTO object `CreateUpdateBookDto`.
* `[HiddenInput]` and `[BindProperty]` are standard ASP.NET Core MVC attributes. `SupportsGet` is used to be able to get the `Id` value from the query string parameter of the request.
* In the `OnGetAsync` method, we get the `BookDto` from the `BookAppService` and this is being mapped to the DTO object `CreateUpdateBookDto`.
* The `OnPostAsync` uses `BookAppService.UpdateAsync(...)` to update the entity.
### Mapping from BookDto to CreateUpdateBookDto
To be able to map the `BookDto` to `CreateUpdateBookDto`, configure a new mapping. To do this, open the `BookStoreWebAutoMapperProfile.cs` in the `Acme.BookStore.Web` project and change it as shown below:
To be able to map the `BookDto` to `CreateUpdateBookDto`, configure a new mapping. To do this, open the `BookStoreWebAutoMapperProfile.cs` file in the `Acme.BookStore.Web` project and change it as shown below:
````csharp
using AutoMapper;
@ -385,16 +385,16 @@ Replace `EditModal.cshtml` content with the following content:
</abp-dynamic-form>
````
This page is very similar to the `CreateModal.cshtml`, except:
This page is very similar to `CreateModal.cshtml`, except:
* It includes an `abp-input` for the `Id` property to store `Id` of the editing book (which is a hidden input).
* It includes an `abp-input` for the `Id` property to store the `Id` of the editing book (which is a hidden input).
* It uses `Books/EditModal` as the post URL.
### Add "Actions" Dropdown to the Table
We will add a dropdown button to the table named *Actions*.
Open the `Pages/Books/Index.js` and replace the content as below:
Open the `Pages/Books/Index.js` file and replace the content as below:
````js
$(function () {
@ -482,8 +482,8 @@ $(function () {
* Added a new `ModalManager` named `editModal` to open the edit modal dialog.
* Added a new column at the beginning of the `columnDefs` section. This column is used for the "*Actions*" dropdown button.
* "*Edit*" action simply calls `editModal.open()` to open the edit dialog.
* `editModal.onResult(...)` callback refreshes the data table when you close the edit modal.
* The "*Edit*" action simply calls `editModal.open()` to open the edit dialog.
* The `editModal.onResult(...)` callback refreshes the data table when you close the edit modal.
You can run the application and edit any book by selecting the edit action on a book.
@ -493,7 +493,7 @@ The final UI looks as below:
## Deleting a Book
Open the `Pages/Books/Index.js` and add a new item to the `rowAction` `items`:
Open the `Pages/Books/Index.js` file and add a new item to the `rowAction` `items`:
````js
{
@ -512,8 +512,8 @@ Open the `Pages/Books/Index.js` and add a new item to the `rowAction` `items`:
}
````
* `confirmMessage` option is used to ask a confirmation question before executing the `action`.
* `acme.bookStore.books.book.delete(...)` method makes an AJAX request to the server to delete a book.
* The `confirmMessage` option is used to ask a confirmation question before executing the `action`.
* The `acme.bookStore.books.book.delete(...)` method makes an AJAX request to the server to delete a book.
* `abp.notify.info()` shows a notification after the delete operation.
Since we've used two new localization texts (`BookDeletionConfirmationMessage` and `SuccessfullyDeleted`) you need to add these to the localization file (`en.json` under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project):
@ -721,10 +721,10 @@ Open `/src/app/book/book.component.html` and make the following changes:
</abp-modal>
```
* Added `New book` button to the card header..
* Added a `New book` button to the card header..
* Added the `abp-modal` which renders a modal to allow user to create a new book. `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits.
You can open your browser and click **New book** button to see the new modal.
You can open your browser and click the **New book** button to see the new modal.
![Empty modal for new book](images/bookstore-empty-new-book-modal.png)
@ -801,11 +801,11 @@ export class BookComponent implements OnInit {
```
* Imported `FormGroup`, `FormBuilder` and `Validators` from `@angular/forms`.
* Added `form: FormGroup` property.
* Added `bookTypes` property as a list of `BookType` enum members. That will be used in form options.
* Added a `form: FormGroup` property.
* Added a `bookTypes` property as a list of `BookType` enum members. That will be used in form options.
* Injected `FormBuilder` into the constructor. [FormBuilder](https://angular.io/api/forms/FormBuilder) provides convenient methods for generating form controls. It reduces the amount of boilerplate needed to build complex forms.
* Added `buildForm` method to the end of the file and executed the `buildForm()` in the `createBook` method.
* Added `save` method.
* Added a `buildForm` method to the end of the file and executed the `buildForm()` in the `createBook` method.
* Added a `save` method.
Open `/src/app/book/book.component.html` and replace `<ng-template #abpBody> </ng-template>` with the following code part:
@ -863,7 +863,7 @@ Also replace `<ng-template #abpFooter> </ng-template>` with the following code p
### Datepicker
We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component. So, need to arrange dependencies related to this component.
We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component. So, we need to arrange the dependencies related to this component.
Open `/src/app/book/book.module.ts` and replace the content as below:
@ -959,7 +959,7 @@ export class BookComponent implements OnInit {
```
* Imported ` NgbDateNativeAdapter` and `NgbDateAdapter`.
* We added a new provider `NgbDateAdapter` that converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details.
* We added a new provider `NgbDateAdapter` that converts the Datepicker value to `Date` type. Check out the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details.
Now, you can open your browser to see the changes:
@ -1054,14 +1054,14 @@ export class BookComponent implements OnInit {
```
* We declared a variable named `selectedBook` as `BookDto`.
* We added `editBook` method. This method fetches the book with the given `id` and sets it to `selectedBook` object.
* We added an `editBook` method. This method fetches the book with the given `id` and sets it to `selectedBook` object.
* We replaced the `buildForm` method so that it creates the form with the `selectedBook` data.
* We replaced the `createBook` method so it sets `selectedBook` to an empty object.
* We changed the `save` method to handle both of create and update operations.
### Add "Actions" Dropdown to the Table
Open the `/src/app/book/book.component.html`  and add the following `ngx-datatable-column` definition as the first column in the `ngx-datatable`:
Open `/src/app/book/book.component.html`  and add the following `ngx-datatable-column` definition as the first column in the `ngx-datatable`:
```html
<ngx-datatable-column
@ -1101,11 +1101,11 @@ Also, change the `ng-template #abpHeader` section as shown below:
</ng-template>
```
This template will show **Edit** text for edit record operation, **New Book** for new record operation in the title.
This template will show the **Edit** text for edit record operation, **New Book** for new record operation in the title.
## Deleting a Book
Open the `/src/app/book/book.component.ts` and inject the `ConfirmationService`.
Open the `/src/app/book/book.component.ts` file and inject the `ConfirmationService`.
Replace the constructor as below:
@ -1137,7 +1137,7 @@ delete(id: string) {
* We injected `ConfirmationService` to the constructor.
* Added a `delete` method.
> See the [Confirmation Popup documentation](../UI/Angular/Confirmation-Service) for more about this service.
> Check out the [Confirmation Popup documentation](../UI/Angular/Confirmation-Service) for more about this service.
### Add a Delete Button
@ -1169,7 +1169,7 @@ Clicking the "Delete" action calls the `delete` method which then shows a confir
In this section, you will learn how to create a new modal dialog form to create a new book. Since we've inherited from the `AbpCrudPageBase`, we only need to develop the view part.
### Add "New Button" Button
### Add a "New Button" Button
Open the `Books.razor` and replace the `<CardHeader>` section with the following code:
@ -1191,7 +1191,7 @@ This will change the card header by adding a "New book" button to the right side
![blazor-add-book-button](images/blazor-add-book-button.png)
Now, we can add a modal that will be opened when we click to the button.
Now, we can add a modal that will be opened when we click the button.
### Book Creation Modal
@ -1259,7 +1259,7 @@ This code requires a service; Inject the `AbpBlazorMessageLocalizerHelper<T>` at
````
* The form implements validation and the `AbpBlazorMessageLocalizerHelper` is used to simply localize the validation messages.
* `CreateModal` object, `CloseCreateModalAsync` and `CreateEntityAsync` method are defined by the base class. See the [Blazorise documentation](https://blazorise.com/docs/) if you want to understand the `Modal` and the other components.
* The `CreateModal` object, `CloseCreateModalAsync` and `CreateEntityAsync` methods are defined by the base class. Check out the [Blazorise documentation](https://blazorise.com/docs/) if you want to understand the `Modal` and the other components.
That's all. Run the application and try to add a new book:
@ -1267,7 +1267,7 @@ That's all. Run the application and try to add a new book:
## Updating a Book
Editing a books is similar to the creating a new book.
Editing a book is similar to creating a new book.
### Actions Dropdown
@ -1287,7 +1287,7 @@ Open the `Books.razor` and add the following `DataGridEntityActionsColumn` secti
* `OpenEditModalAsync` is defined in the base class which takes the entity (book) to edit.
`DataGridEntityActionsColumn` component is used to show an "Actions" dropdown for each row in the `DataGrid`. `DataGridEntityActionsColumn` shows a **single button** instead of a dropdown if there is only one available action inside it:
The `DataGridEntityActionsColumn` component is used to show an "Actions" dropdown for each row in the `DataGrid`. The `DataGridEntityActionsColumn` shows a **single button** instead of a dropdown if there is only one available action inside it:
![blazor-edit-book-action](images/blazor-edit-book-action-2.png)
@ -1384,7 +1384,7 @@ You can now run the application and try to edit a book.
## Deleting a Book
Open the `Books.razor` page and add the following `EntityAction` under the "Edit" action inside the `EntityActions`:
Open the `Books.razor` page and add the following `EntityAction` code under the "Edit" action inside `EntityActions`:
````xml
<EntityAction TItem="BookDto"
@ -1405,7 +1405,7 @@ Run the application and try to delete a book.
## Full CRUD UI Code
Here the complete code to create the book management CRUD page, that has been developed in the last two parts:
Here's the complete code to create the book management CRUD page, that has been developed in the last two parts:
````xml
@page "/books"
@ -1596,4 +1596,4 @@ Here the complete code to create the book management CRUD page, that has been de
## The Next Part
See the [next part](Part-4.md) of this tutorial.
Check out the [next part](Part-4.md) of this tutorial.

@ -32,26 +32,23 @@ namespace MyCompany.MyProject
### ASP.NET Core 应用程序
如下所示, 在 **Startup.cs** 文件中调用 `UseAutofac()`:
如下所示, 在 **Program.cs** 文件中调用 `UseAutofac()`:
````csharp
public class Startup
public class Program
{
public IServiceProvider ConfigureServices(IServiceCollection services)
public static int Main(string[] args)
{
services.AddApplication<MyWebModule>(options =>
{
//Integrate Autofac!
options.UseAutofac();
});
return services.BuildServiceProviderFromFactory();
CreateHostBuilder(args).Build().Run();
}
public void Configure(IApplicationBuilder app)
{
app.InitializeApplication();
}
internal static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.UseAutofac(); //Integrate Autofac!
}
````

@ -67,6 +67,33 @@ public class MyLogWorker : HangfireBackgroundWorkerBase
> 你可以直接实现 `IHangfireBackgroundWorker`, 但是 `HangfireBackgroundWorkerBase` 提供了一些有用的属性,例如 `Logger`.
### UnitOfWork
使用 `UnitOfWorkAttribute` 你需要为工作者定义一个接口:
```csharp
public interface IMyLogWorker : IHangfireBackgroundWorker
{
}
[ExposeServices(typeof(IMyLogWorker))]
public class MyLogWorker : HangfireBackgroundWorkerBase, IMyLogWorker
{
public MyLogWorker()
{
RecurringJobId = nameof(MyLogWorker);
CronExpression = Cron.Daily();
}
[UnitOfWork]
public override Task DoWorkAsync()
{
Logger.LogInformation("Executed MyLogWorker..!");
return Task.CompletedTask;
}
}
```
## 注册到后台工作者管理器
创建一个后台工作者后, 你应该添加到 `IBackgroundWorkerManager`, 最常用的地方是在你模块类的 `OnApplicationInitialization` 方法中:
@ -79,6 +106,9 @@ public class MyModule : AbpModule
ApplicationInitializationContext context)
{
context.AddBackgroundWorker<MyLogWorker>();
//如果定义了接口
//context.AddBackgroundWorker<IMyLogWorker>();
}
}
````

@ -1,3 +0,0 @@
# CMS Kit Module
TODO

@ -0,0 +1,3 @@
# CMS Kit: Blogging
TODO...

@ -0,0 +1,3 @@
# CMS Kit: Comments
TODO...

@ -0,0 +1,79 @@
# 内容管理系统套件模块
此模块为您的应用程序提供内容管理系统 (Content Management System, CMS) 功能. 它提供 **核心构建块** 和完整工作的 **子系统**, 以创建启用 CMS 功能的您自己的网站, 或出于任何目的使用网站中的构建块.
> **此模块目前仅适用于 MVC / Razor 页面 UI**. 虽然没有官方的 Blazor 软件包, 但它也可以在 Blazor 服务器 UI 中工作, 因为实际上 Blazor 服务器 UI 实际上是一个运行在 ASP.NET Core MVC / Razor 页面应用程序的混合型应用程序.
目前提供以下功能:
* 提供 [**页面**](Pages.md) 管理系统来管理具有动态 URL 的动态页面.
* 提供 [**博客**](Blogging.md) 系统来创建发表具有多种博客支持的博客文章.
* 提供 [**标签**](Tags.md) 系统来标记任何资源, 如博客文章.
* 提供 [**评论**](Comments.md) 系统来添加对任何资源的评论功能, 如博客文章或产品评价页面.
* 提供 [**反应**](Reactions.md) 系统来添加对任何资源的反应 (表情符号) 功能, 如博客文章或评论.
* 提供 [**评级**](Ratings.md) 系统来添加对任何资源的评级功能.
* 提供 [**菜单**](Menus.md) 系统来动态管理公共菜单.
点击功能以了解和学习如何去使用它.
所有功能均可单独使用. 如果你禁用了一个功能, 则在 [全局功能](../../Global-Features.md) 系统的帮助下, 该功能会从你的应用程序甚至数据库表中完全消失.
## 预备要求
- 此模块依赖于 [Blob 存储](../../Blob-Storing.md) 模块来保存媒体内容.
> 确保 `BlobStoring` 模块已安装并至少正确地配置了一个提供程序. 请查阅 [文档](../../Blob-Storing.md) 了解更多信息.
- CMS Kit 使用 [分布式缓存](../../Caching.md) 来提高响应速度.
> 强烈建议在分布式/集群部署中为实现数据一致性使用分布式缓存, 如 [Redis](../../Redis-Cache.md).
## 如何安装
可以使用 [ABP CLI](../../CLI.md) 的 `add-module` 命令为解决方案安装模块. 您可以使用以下命令在命令行中安装 CMS Kit 模块:
```bash
abp add-module Volo.CmsKit
```
> 默认情况下, Cms-Kit `GlobalFeature` 被禁用. 因此初始迁移将为空. 所以, 当你使用 EF Core 安装时,你可以添加 `--skip-db-migrations` 命令来跳过迁移. 启用 Cms-Kit 全局功能后, 请添加新的迁移.
安装过程完成后, 在您的解决方案 `Domain.Shared` 项目中打开 `GlobalFeatureConfigurator` 类, 并将以下代码写入 `Configure` 方法中, 以启用 CMS Kit 模块的全部功能.
```csharp
GlobalFeatureManager.Instance.Modules.CmsKit(cmsKit =>
{
cmsKit.EnableAll();
});
```
你可能更愿意逐个启用这些功能, 而不是启用全部功能. 以下示例仅启用了 [标签](Tags.md) 和 [评论](Comments.md) 功能:
````csharp
GlobalFeatureManager.Instance.Modules.CmsKit(cmsKit =>
{
cmsKit.Tags.Enable();
cmsKit.Comments.Enable();
});
````
> 如果你使用 EF Core, 不要忘记添加一个新的迁移并更新你的数据库.
## 软件包
此模块遵循 [模块开发最佳实践指南](https://docs.abp.io/zh-Hans/abp/latest/Best-Practices/Index), 由多个 NuGet 和 NPM 软件包组成. 如果你想了解软件包及其之间的关系, 请参阅指南.
CMS Kit 软件包专为各种使用场景而设计. 如果您查阅了 [CMS Kit 软件包](https://www.nuget.org/packages?q=Volo.CmsKit) 您将看到一些有 `Admin``Public` 后缀的软件包. 该模块有两个应用程序层, 原因是他们可能被用于不同类型的应用程序. 这些应用程序层仅使用一个领域层.
- `Volo.CmsKit.Admin.*` 软件包包括管理员 (后台) 应用程序所必须的功能.
- `Volo.CmsKit.Public.*` 软件包包括被用于用户阅读博客文章和发表评论的公共网站上的功能.
- `Volo.CmsKit.*` (不带 Admin/Public 后缀) 软件包称为统一包. 统一包分别是添加 Admin 和 Public (相关层的) 软件包的快照. 如果您有一个用于管理和公共网站的单应用程序, 您可以使用这些软件包.
## 内部结构
### 表/集合 前缀&架构
所有表/集合使用 `Cms` 作为默认前缀. 如果需要更改表的前缀或者设置一个架构名称 (如果你的数据库提供程序支持), 请在 `CmsKitDbProperties` 类中设置静态属性.
### 连接字符串
此模块使用 `CmsKit` 作为连接字符串的名称. 如果您未使用此名称定义连接字符串, 它将回退为 `Default` 连接字符串.
有关详细信息, 请参阅 [连接字符串](https://docs.abp.io/en/abp/latest/Connection-Strings) 文档.

@ -0,0 +1,3 @@
# CMS Kit: Pages
TODO...

@ -0,0 +1,3 @@
# CMS Kit: Pages
TODO...

@ -0,0 +1,3 @@
# Rating System
TODO...

@ -0,0 +1,3 @@
# Reaction System
TODO...

@ -0,0 +1,3 @@
# Tag Management
TODO...

@ -4,7 +4,7 @@
## 如何安装
当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时,此模块将被预安装(作为 NuGet/NPM 包)。你可以继续用其作为包并轻松地获取更新,也可以将其源代码包含在解决方案中(请参阅 `get-source` [CLI](../CLI.md))以开发自定义模块
当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时, 此模块将被预安装 (作为 NuGet/NPM 包). 你可以继续用其作为包并轻松地获取更新, 也可以将其源代码包含在解决方案中(请参阅 `get-source` [CLI](../CLI.md))以开发自定义模块.
### 源代码

@ -12,7 +12,7 @@
## 如何安装
当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时,此模块将被预安装(作为 NuGet/NPM 包)。你可以继续用其作为包并轻松地获取更新,也可以将其源代码包含在解决方案中(请参阅 `get-source` [CLI](../CLI.md))以开发自定义模块
当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时, 此模块将被预安装(作为 NuGet/NPM 包). 你可以继续用其作为包并轻松地获取更新, 也可以将其源代码包含在解决方案中(请参阅 `get-source` [CLI](../CLI.md))以开发自定义模块.
### 源代码

@ -1 +1,574 @@
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-7.md)
- [Part 8: 作者: 应用服务层](本章)
- [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`
## 简介
这章阐述如何为前一章介绍的 `作者` 实体创建应用服务层.
## IAuthorAppService
我们首先创建 [应用服务](../Application-Services.md) 接口和相关的 [DTO](../Data-Transfer-Objects.md)s. 在 `Acme.BookStore.Application.Contracts` 项目的 `Authors` 命名空间 (文件夹) 创建一个新接口 `IAuthorAppService`:
````csharp
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Acme.BookStore.Authors
{
public interface IAuthorAppService : IApplicationService
{
Task<AuthorDto> GetAsync(Guid id);
Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input);
Task<AuthorDto> CreateAsync(CreateAuthorDto input);
Task UpdateAsync(Guid id, UpdateAuthorDto input);
Task DeleteAsync(Guid id);
}
}
````
* `IApplicationService` 是一个常规接口, 所有应用服务都继承自它, 所以 ABP 框架可以识别它们.
* 在 `Author` 实体中定义标准方法用于CRUD操作.
* `PagedResultDto` 是一个ABP框架中预定义的 DTO 类. 它拥有一个 `Items` 集合 和一个 `TotalCount` 属性, 用于返回分页结果.
* 优先从 `CreateAsync` 方法返回 `AuthorDto` (新创建的作者), 虽然在这个程序中没有这么做 - 这里只是展示一种不同用法.
这个类使用下面定义的DTOs (为你的项目创建它们).
### AuthorDto
````csharp
using System;
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Authors
{
public class AuthorDto : EntityDto<Guid>
{
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
}
}
````
* `EntityDto<T>` 只有一个类型为指定泛型参数的 `Id` 属性. 你可以自己创建 `Id` 属性, 而不是继承自 `EntityDto<T>`.
### GetAuthorListDto
````csharp
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Authors
{
public class GetAuthorListDto : PagedAndSortedResultRequestDto
{
public string Filter { get; set; }
}
}
````
* `Filter` 用于搜索作者. 它可以是 `null` (或空字符串) 以获得所有用户.
* `PagedAndSortedResultRequestDto` 具有标准分页和排序属性: `int MaxResultCount`, `int SkipCount``string Sorting`.
> ABP 框架拥有这些基本的DTO类以简化并标准化你的DTOs. 参阅 [DTO 文档](../Data-Transfer-Objects.md) 获得所有DTO类的详细信息.
### CreateAuthorDto
````csharp
using System;
using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore.Authors
{
public class CreateAuthorDto
{
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; }
[Required]
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
}
}
````
数据标记特性可以用来验证DTO. 参阅 [验证文档](../Validation.md) 获得详细信息.
### UpdateAuthorDto
````csharp
using System;
using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore.Authors
{
public class UpdateAuthorDto
{
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; }
[Required]
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
}
}
````
> 我们可以在创建和更新操作间分享 (重用) 相同的DTO. 虽然可以这么做, 但我们推荐为这些操作创建不同的DTOs, 因为我们发现随着时间的推移, 它们通常会变得有差异. 所以, 与紧耦合相比, 代码重复也是合理的.
## AuthorAppService
是时候实现 `IAuthorAppService` 接口了. 在 `Acme.BookStore.Application` 项目的 `Authors` 命名空间 (文件夹) 中创建一个新类 `AuthorAppService` :
````csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Acme.BookStore.Permissions;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Authors
{
[Authorize(BookStorePermissions.Authors.Default)]
public class AuthorAppService : BookStoreAppService, IAuthorAppService
{
private readonly IAuthorRepository _authorRepository;
private readonly AuthorManager _authorManager;
public AuthorAppService(
IAuthorRepository authorRepository,
AuthorManager authorManager)
{
_authorRepository = authorRepository;
_authorManager = authorManager;
}
//...SERVICE METHODS WILL COME HERE...
}
}
````
* `[Authorize(BookStorePermissions.Authors.Default)]` 是一个检查权限(策略)的声明式方法, 用来给当前用户授权. 参阅 [授权文档](../Authorization.md) 获得详细信息. `BookStorePermissions` 类在后文会被更新, 现在不需要担心编译错误.
* 由 `BookStoreAppService` 派生, 这个类是一个简单基类, 可以做为模板. 它继承自标准的 `ApplicationService` 类.
* 实现上面定义的 `IAuthorAppService` .
* 注入 `IAuthorRepository``AuthorManager` 以使用服务方法.
现在, 我们逐个介绍服务方法. 复制这些方法到 `AuthorAppService` 类.
### GetAsync
````csharp
public async Task<AuthorDto> GetAsync(Guid id)
{
var author = await _authorRepository.GetAsync(id);
return ObjectMapper.Map<Author, AuthorDto>(author);
}
````
这个方法根据 `Id` 获得 `Author` 实体, 使用 [对象到对象映射](../Object-To-Object-Mapping.md) 转换为 `AuthorDto`. 这需要配置AutoMapper, 后面会介绍.
### GetListAsync
````csharp
public async Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input)
{
if (input.Sorting.IsNullOrWhiteSpace())
{
input.Sorting = nameof(Author.Name);
}
var authors = await _authorRepository.GetListAsync(
input.SkipCount,
input.MaxResultCount,
input.Sorting,
input.Filter
);
var totalCount = input.Filter == null
? await _authorRepository.CountAsync()
: await _authorRepository.CountAsync(
author => author.Name.Contains(input.Filter));
return new PagedResultDto<AuthorDto>(
totalCount,
ObjectMapper.Map<List<Author>, List<AuthorDto>>(authors)
);
}
````
* 为处理客户端没有设置的情况, 在方法的开头设置默认排序是 "根据作者名".
* 使用 `IAuthorRepository.GetListAsync` 从数据库中获得分页的, 排序的和过滤的作者列表. 我们已经在教程的前一章中实现了它. 再一次强调, 实际上不需要创建这个方法, 因为我们可以从数据库中直接查询, 这里只是演示如何创建自定义repository方法.
* 直接查询 `AuthorRepository` , 得到作者的数量. 如果客户端发送了过滤条件, 会得到过滤后的作者数量.
* 最后, 通过映射 `Author` 列表到 `AuthorDto` 列表, 返回分页后的结果.
### CreateAsync
````csharp
[Authorize(BookStorePermissions.Authors.Create)]
public async Task<AuthorDto> CreateAsync(CreateAuthorDto input)
{
var author = await _authorManager.CreateAsync(
input.Name,
input.BirthDate,
input.ShortBio
);
await _authorRepository.InsertAsync(author);
return ObjectMapper.Map<Author, AuthorDto>(author);
}
````
* `CreateAsync` 需要 `BookStorePermissions.Authors.Create` 权限 (另外包括 `AuthorAppService` 类声明的 `BookStorePermissions.Authors.Default` 权限).
* 使用 `AuthorManager` (领域服务) 创建新作者.
* 使用 `IAuthorRepository.InsertAsync` 插入新作者到数据库.
* 使用 `ObjectMapper` 返回 `AuthorDto` , 代表新创建的作者.
> **DDD提示**: 一些开发者可能会发现可以在 `_authorManager.CreateAsync` 插入新实体. 我们认为把它留给应用层是更好的设计, 因为应用层更了解应该何时插入实体到数据库(在插入实体前可能需要额外的工作. 如果在领域层插入, 可能需要额外的更新操作). 但是, 你拥有最终的决定权.
### UpdateAsync
````csharp
[Authorize(BookStorePermissions.Authors.Edit)]
public async Task UpdateAsync(Guid id, UpdateAuthorDto input)
{
var author = await _authorRepository.GetAsync(id);
if (author.Name != input.Name)
{
await _authorManager.ChangeNameAsync(author, input.Name);
}
author.BirthDate = input.BirthDate;
author.ShortBio = input.ShortBio;
await _authorRepository.UpdateAsync(author);
}
````
* `UpdateAsync` 需要额外的 `BookStorePermissions.Authors.Edit` 权限.
* 使用 `IAuthorRepository.GetAsync` 从数据库中获得作者实体. 如果给定的id没有找到作者, `GetAsync` 抛出 `EntityNotFoundException`, 这在web应用程序中导致一个 `404` HTTP 状态码. 在更新操作中先获取实体再更新它, 是一个好的实践.
* 如果客户端请求, 使用 `AuthorManager.ChangeNameAsync` (领域服务方法) 修改作者姓名.
* 因为没有任何业务逻辑, 直接更新 `BirthDate``ShortBio`, 它们可以接受任何值.
* 最后, 调用 `IAuthorRepository.UpdateAsync` 更新实体到数据库.
{{if DB == "EF"}}
> **EF Core 提示**: Entity Framework Core 拥有 **change tracking** 系统并在unit of work 结束时 **自动保存** 任何修改到实体 (你可以简单地认为APB框架在方法结束时自动调用 `SaveChanges`). 所以, 即使你在方法结束时没有调用 `_authorRepository.UpdateAsync(...)` , 它依然可以工作. 如果你不考虑以后修改EF Core, 你可以移除这一行.
{{end}}
### DeleteAsync
````csharp
[Authorize(BookStorePermissions.Authors.Delete)]
public async Task DeleteAsync(Guid id)
{
await _authorRepository.DeleteAsync(id);
}
````
* `DeleteAsync` 需要额外的 `BookStorePermissions.Authors.Delete` 权限.
* 直接使用repository的 `DeleteAsync` 方法.
## 权限定义
你还不能编译代码, 因为它需要 `BookStorePermissions` 类定义中一些常数.
打开 `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";
}
// *** ADDED a NEW NESTED CLASS ***
public static class Authors
{
public const string Default = GroupName + ".Authors";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
}
}
````
然后打开同一项目中的 `BookStorePermissionDefinitionProvider`, 在 `Define` 方法的结尾加入以下行:
````csharp
var authorsPermission = bookStoreGroup.AddPermission(
BookStorePermissions.Authors.Default, L("Permission:Authors"));
authorsPermission.AddChild(
BookStorePermissions.Authors.Create, L("Permission:Authors.Create"));
authorsPermission.AddChild(
BookStorePermissions.Authors.Edit, L("Permission:Authors.Edit"));
authorsPermission.AddChild(
BookStorePermissions.Authors.Delete, L("Permission:Authors.Delete"));
````
最后, 在 `Acme.BookStore.Domain.Shared` 项目中的 `Localization/BookStore/en.json` 加入以下项, 用以本地化权限名称:
````csharp
"Permission:Authors": "Author Management",
"Permission:Authors.Create": "Creating new authors",
"Permission:Authors.Edit": "Editing the authors",
"Permission:Authors.Delete": "Deleting the authors"
````
## 对象到对象映射
`AuthorAppService` 使用 `ObjectMapper``Author` 对象 转换为 `AuthorDto` 对象. 所以, 我们需要在 AutoMapper 配置中定义映射.
打开 `Acme.BookStore.Application` 项目中的 `BookStoreApplicationAutoMapperProfile` 类, 加入以下行到构造函数:
````csharp
CreateMap<Author, AuthorDto>();
````
## 种子数据
如同图书管理部分所做的, 在数据库中生成一些初始作者实体. 不仅当第一次运行应用程序时是有用的, 对自动化测试也是很有用的.
打开 `Acme.BookStore.Domain` 项目中的 `BookStoreDataSeederContributor`, 修改文件内容如下:
````csharp
using System;
using System.Threading.Tasks;
using Acme.BookStore.Authors;
using Acme.BookStore.Books;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore
{
public class BookStoreDataSeederContributor
: IDataSeedContributor, ITransientDependency
{
private readonly IRepository<Book, Guid> _bookRepository;
private readonly IAuthorRepository _authorRepository;
private readonly AuthorManager _authorManager;
public BookStoreDataSeederContributor(
IRepository<Book, Guid> bookRepository,
IAuthorRepository authorRepository,
AuthorManager authorManager)
{
_bookRepository = bookRepository;
_authorRepository = authorRepository;
_authorManager = authorManager;
}
public async Task SeedAsync(DataSeedContext context)
{
if (await _bookRepository.GetCountAsync() <= 0)
{
await _bookRepository.InsertAsync(
new Book
{
Name = "1984",
Type = BookType.Dystopia,
PublishDate = new DateTime(1949, 6, 8),
Price = 19.84f
},
autoSave: true
);
await _bookRepository.InsertAsync(
new Book
{
Name = "The Hitchhiker's Guide to the Galaxy",
Type = BookType.ScienceFiction,
PublishDate = new DateTime(1995, 9, 27),
Price = 42.0f
},
autoSave: true
);
}
// ADDED SEED DATA FOR AUTHORS
if (await _authorRepository.GetCountAsync() <= 0)
{
await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"George Orwell",
new DateTime(1903, 06, 25),
"Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)."
)
);
await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"Douglas Adams",
new DateTime(1952, 03, 11),
"Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'."
)
);
}
}
}
}
````
{{if DB=="EF"}}
你现在可以运行 `.DbMigrator` 控制台应用程序, **迁移** **数据库 schema** 并生成 **种子** 初始数据.
{{else if DB=="Mongo"}}
你现在可以运行 `.DbMigrator` 控制台应用程序, **迁移** **数据库 schema** 并生成 **种子** 初始数据.
{{end}}
## 测试作者应用服务
最后, 你可以为 `IAuthorAppService` 写一些测试. 在 `Acme.BookStore.Application.Tests` 项目的 `Authors` 命名空间(文件夹)中加入一个名为 `AuthorAppService_Tests` 新类:
````csharp
using System;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
namespace Acme.BookStore.Authors
{ {{if DB=="Mongo"}}
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}}
public class AuthorAppService_Tests : BookStoreApplicationTestBase
{
private readonly IAuthorAppService _authorAppService;
public AuthorAppService_Tests()
{
_authorAppService = GetRequiredService<IAuthorAppService>();
}
[Fact]
public async Task Should_Get_All_Authors_Without_Any_Filter()
{
var result = await _authorAppService.GetListAsync(new GetAuthorListDto());
result.TotalCount.ShouldBeGreaterThanOrEqualTo(2);
result.Items.ShouldContain(author => author.Name == "George Orwell");
result.Items.ShouldContain(author => author.Name == "Douglas Adams");
}
[Fact]
public async Task Should_Get_Filtered_Authors()
{
var result = await _authorAppService.GetListAsync(
new GetAuthorListDto {Filter = "George"});
result.TotalCount.ShouldBeGreaterThanOrEqualTo(1);
result.Items.ShouldContain(author => author.Name == "George Orwell");
result.Items.ShouldNotContain(author => author.Name == "Douglas Adams");
}
[Fact]
public async Task Should_Create_A_New_Author()
{
var authorDto = await _authorAppService.CreateAsync(
new CreateAuthorDto
{
Name = "Edward Bellamy",
BirthDate = new DateTime(1850, 05, 22),
ShortBio = "Edward Bellamy was an American author..."
}
);
authorDto.Id.ShouldNotBe(Guid.Empty);
authorDto.Name.ShouldBe("Edward Bellamy");
}
[Fact]
public async Task Should_Not_Allow_To_Create_Duplicate_Author()
{
await Assert.ThrowsAsync<AuthorAlreadyExistsException>(async () =>
{
await _authorAppService.CreateAsync(
new CreateAuthorDto
{
Name = "Douglas Adams",
BirthDate = DateTime.Now,
ShortBio = "..."
}
);
});
}
//TODO: Test other methods...
}
}
````
完成应用服务方法的测试, 它们应该很容易理解.
## 下一章
查看本教程的[下一章](Part-9.md).

File diff suppressed because it is too large Load Diff

@ -692,7 +692,7 @@
},
{
"text": "CMS Kit",
"path": "Modules/Cms-Kit.md"
"path": "Modules/Cms-Kit/Index.md"
},
{
"text": "文档",

@ -37,7 +37,7 @@ public partial class PageHeader : ComponentBase
ToolbarItemRenders = new List<RenderFragment>();
}
protected override async Task OnParametersSetAsync()
protected async override Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
if (Toolbar != null)
@ -65,7 +65,7 @@ public partial class PageHeader : ComponentBase
}
}
protected override async Task OnInitializedAsync()
protected async override Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}

@ -12,5 +12,10 @@ public class BootstrapScriptContributor : BundleContributor
{
context.Files.AddIfNotContains("/libs/bootstrap/js/bootstrap.bundle.js");
context.Files.AddIfNotContains("/libs/bootstrap/js/bootstrap.enable.tooltips.everywhere.js");
if (context.FileProvider.GetFileInfo("/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js").Exists)
{
context.Files.AddIfNotContains("/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js");
}
}
}

@ -1,7 +1,6 @@
using System;
using System.Linq;
using AutoMapper;
using AutoMapper.Internal;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
@ -19,7 +18,8 @@ public class AbpAutoMapperConventionalRegistrar : DefaultConventionalRegistrar
protected override bool IsConventionalRegistrationDisabled(Type type)
{
return !OpenTypes.Any(type.ImplementsGenericInterface) || base.IsConventionalRegistrationDisabled(type);
return !type.GetInterfaces().Any(x => x.IsGenericType && OpenTypes.Contains(x.GetGenericTypeDefinition())) ||
base.IsConventionalRegistrationDisabled(type);
}
protected override ServiceLifetime? GetDefaultLifeTimeOrNull(Type type)

@ -4,6 +4,7 @@ using System.Threading;
using System.Threading.Tasks;
using Hangfire;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
using Volo.Abp.Threading;
namespace Volo.Abp.BackgroundWorkers.Hangfire;
@ -25,14 +26,14 @@ public class HangfireBackgroundWorkerManager : IBackgroundWorkerManager, ISingle
{
if (worker is IHangfireBackgroundWorker hangfireBackgroundWorker)
{
var unProxyWorker = ProxyHelper.UnProxy(hangfireBackgroundWorker);
if (hangfireBackgroundWorker.RecurringJobId.IsNullOrWhiteSpace())
{
RecurringJob.AddOrUpdate(() => hangfireBackgroundWorker.DoWorkAsync(),
hangfireBackgroundWorker.CronExpression);
RecurringJob.AddOrUpdate(() => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(),hangfireBackgroundWorker.CronExpression);
}
else
{
RecurringJob.AddOrUpdate(hangfireBackgroundWorker.RecurringJobId,() => hangfireBackgroundWorker.DoWorkAsync(),
RecurringJob.AddOrUpdate(hangfireBackgroundWorker.RecurringJobId,() => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(),
hangfireBackgroundWorker.CronExpression);
}
}
@ -64,7 +65,7 @@ public class HangfireBackgroundWorkerManager : IBackgroundWorkerManager, ISingle
return Task.CompletedTask;
}
var adapterType = typeof(HangfirePeriodicBackgroundWorkerAdapter<>).MakeGenericType(worker.GetType());
var adapterType = typeof(HangfirePeriodicBackgroundWorkerAdapter<>).MakeGenericType(ProxyHelper.GetUnProxiedType(worker));
var workerAdapter = Activator.CreateInstance(adapterType) as IHangfireBackgroundWorker;
RecurringJob.AddOrUpdate(() => workerAdapter.DoWorkAsync(), GetCron(period.Value));

@ -1,8 +1,9 @@
using System;
using System;
using System.Threading;
using System.Threading.Tasks;
using Quartz;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
using Volo.Abp.Threading;
namespace Volo.Abp.BackgroundWorkers.Quartz;
@ -56,7 +57,7 @@ public class QuartzBackgroundWorkerManager : IBackgroundWorkerManager, ISingleto
}
else
{
var adapterType = typeof(QuartzPeriodicBackgroundWorkerAdapter<>).MakeGenericType(worker.GetType());
var adapterType = typeof(QuartzPeriodicBackgroundWorkerAdapter<>).MakeGenericType(ProxyHelper.GetUnProxiedType(worker));
var workerAdapter = Activator.CreateInstance(adapterType) as IQuartzBackgroundWorkerAdapter;

@ -1,7 +1,8 @@
using System;
using System;
using System.Reflection;
using System.Threading.Tasks;
using Quartz;
using Volo.Abp.DynamicProxy;
using Volo.Abp.Threading;
namespace Volo.Abp.BackgroundWorkers.Quartz;
@ -26,16 +27,16 @@ public class QuartzPeriodicBackgroundWorkerAdapter<TWorker> : QuartzBackgroundWo
public void BuildWorker(IBackgroundWorker worker)
{
int? period;
var workerType = worker.GetType();
var workerType = ProxyHelper.GetUnProxiedType(worker);
if (worker is AsyncPeriodicBackgroundWorkerBase or PeriodicBackgroundWorkerBase)
{
if (typeof(TWorker) != worker.GetType())
if (typeof(TWorker) != workerType)
{
throw new ArgumentException($"{nameof(worker)} type is different from the generic type");
}
var timer = worker.GetType().GetProperty("Timer", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(worker);
var timer = workerType.GetProperty("Timer", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(worker);
if (worker is AsyncPeriodicBackgroundWorkerBase)
{

@ -212,7 +212,7 @@ public abstract class AbpCrudPageBase<
EntityActions = new EntityActionDictionary();
}
protected override async Task OnInitializedAsync()
protected async override Task OnInitializedAsync()
{
await SetPermissionsAsync();
await SetEntityActionsAsync();
@ -220,7 +220,7 @@ public abstract class AbpCrudPageBase<
await InvokeAsync(StateHasChanged);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
protected async override Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
@ -350,6 +350,7 @@ public abstract class AbpCrudPageBase<
protected virtual Task CloseCreateModalAsync()
{
NewEntity = new TCreateViewModel();
return InvokeAsync(CreateModal.Hide);
}
@ -464,6 +465,7 @@ public abstract class AbpCrudPageBase<
protected virtual async Task OnCreatedEntityAsync()
{
NewEntity = new TCreateViewModel();
await GetEntitiesAsync();
await InvokeAsync(CreateModal.Hide);

@ -2,10 +2,20 @@
@typeparam TResourceType
@using Volo.Abp.BlazoriseUI
@using Volo.Abp.Localization
@inherits ExtensionPropertyComponentBase<TEntity, TResourceType>
@if (PropertyInfo != null && Entity != null)
{
<Field>
<Check TValue="bool" @bind-Checked="@Value">@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)</Check>
</Field>
}
<Validation Validator="@Validate" MessageLocalizer="@LH.Localize">
<Field>
<Check TValue="bool" @bind-Checked="@Value">
<ChildContent>
@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)
</ChildContent>
<Feedback>
<ValidationError/>
</Feedback>
</Check>
</Field>
</Validation>
}

@ -1,22 +1,10 @@
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending;
using Volo.Abp.Data;
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending;
public partial class CheckExtensionProperty<TEntity, TResourceType> : ComponentBase
public partial class CheckExtensionProperty<TEntity, TResourceType>
where TEntity : IHasExtraProperties
{
[Inject]
public IStringLocalizerFactory StringLocalizerFactory { get; set; }
[Parameter]
public TEntity Entity { get; set; }
[Parameter]
public ObjectExtensionPropertyInfo PropertyInfo { get; set; }
protected bool Value {
get {
return PropertyInfo.GetInputValueOrDefault<bool>(Entity.GetProperty(PropertyInfo.Name));

@ -3,15 +3,21 @@
@using Volo.Abp.BlazoriseUI
@using Volo.Abp.Localization
@using Volo.Abp.ObjectExtending
@inherits ExtensionPropertyComponentBase<TEntity, TResourceType>
@if (PropertyInfo != null && Entity != null)
{
<Field>
<FieldLabel>@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)</FieldLabel>
<DateEdit TValue="DateTime?"
InputMode="@(PropertyInfo.IsDate() ? DateInputMode.Date : DateInputMode.DateTime)"
Pattern="@PropertyInfo.GetDateEditInputFormatOrNull()"
@bind-Date="@Value">
</DateEdit>
</Field>
<Validation Validator="@Validate" MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)</FieldLabel>
<DateEdit TValue="DateTime?"
InputMode="@(PropertyInfo.IsDate() ? DateInputMode.Date : DateInputMode.DateTime)"
Pattern="@PropertyInfo.GetDateEditInputFormatOrNull()"
@bind-Date="@Value">
<Feedback>
<ValidationError/>
</Feedback>
</DateEdit>
</Field>
</Validation>
}

@ -5,23 +5,16 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Blazorise;
using Volo.Abp.AspNetCore.Components.Web;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending;
public partial class DateTimeExtensionProperty<TEntity, TResourceType> : ComponentBase
public partial class DateTimeExtensionProperty<TEntity, TResourceType>
where TEntity : IHasExtraProperties
{
[Inject]
public IStringLocalizerFactory StringLocalizerFactory { get; set; }
[Parameter]
public TEntity Entity { get; set; }
[Parameter]
public ObjectExtensionPropertyInfo PropertyInfo { get; set; }
protected DateTime? Value {
get {
return PropertyInfo.GetInputValueOrDefault<DateTime?>(Entity.GetProperty(PropertyInfo.Name));

@ -1,7 +1,6 @@
@typeparam TEntityType
@typeparam TResourceType
@using Volo.Abp.ObjectExtending
@using Volo.Abp.Localization
@using Volo.Abp.Data
@{
@ -13,11 +12,11 @@
{
if (propertyInfo.Type.IsEnum)
{
<SelectExtensionProperty PropertyInfo="@propertyInfo" Entity="@Entity" TEntity="TEntityType" TResourceType="TResourceType" />
<SelectExtensionProperty PropertyInfo="@propertyInfo" Entity="@Entity" TEntity="TEntityType" TResourceType="TResourceType" LH="@LH" />
}
else if (!propertyInfo.Lookup.Url.IsNullOrEmpty())
{
<LookupExtensionProperty PropertyInfo="@propertyInfo" Entity="@Entity" TEntity="TEntityType" TResourceType="TResourceType" />
<LookupExtensionProperty PropertyInfo="@propertyInfo" Entity="@Entity" TEntity="TEntityType" TResourceType="TResourceType" LH="@LH" />
}
else
{
@ -25,6 +24,7 @@
__builder.OpenComponent(0, inputType.MakeGenericType(new[] { typeof(TEntityType), typeof(TResourceType) }));
__builder.AddAttribute(1, "PropertyInfo", propertyInfo);
__builder.AddAttribute(2, "Entity", Entity);
__builder.AddAttribute(3, "LH", LH);
__builder.CloseComponent();
}
}

@ -0,0 +1,83 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Blazorise;
using Blazorise.Utilities;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
using Volo.Abp.AspNetCore.Components.Web;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending;
public abstract class ExtensionPropertyComponentBase<TEntity, TResourceType> : OwningComponentBase
where TEntity : IHasExtraProperties
{
[Inject]
public IStringLocalizerFactory StringLocalizerFactory { get; set; }
[Inject]
public IValidationMessageLocalizerAttributeFinder ValidationMessageLocalizerAttributeFinder { get; set; }
[Parameter]
public TEntity Entity { get; set; }
[Parameter]
public ObjectExtensionPropertyInfo PropertyInfo { get; set; }
[Parameter]
public AbpBlazorMessageLocalizerHelper<TResourceType> LH { get; set; }
protected virtual void Validate(ValidatorEventArgs e)
{
e.Status = ValidationStatus.Success;
var validationAttributes = PropertyInfo.GetValidationAttributes();
var validationContext = new ValidationContext(Entity)
{
DisplayName = PropertyInfo.Name,
MemberName = PropertyInfo.Name
};
foreach (var validationAttribute in validationAttributes)
{
var result = validationAttribute.GetValidationResult(e.Value, validationContext);
if (result == ValidationResult.Success || result == null)
{
continue;
}
var errorMessage = result.ErrorMessage;
if (LH != null)
{
var formattedErrorMessage = GetDefaultErrorMessage(validationAttribute);
var errorMessageString = ValidationAttributeHelper.RevertErrorMessagePlaceholders(formattedErrorMessage);
var errorMessageArguments = ValidationMessageLocalizerAttributeFinder.FindAll(errorMessage, errorMessageString)
?.OrderBy(x => x.Index)
?.Select(x => x.Argument);
errorMessage = LH.Localize(errorMessageString, errorMessageArguments);
}
e.MemberNames = result.MemberNames;
e.Status = ValidationStatus.Error;
e.ErrorText = errorMessage;
break;
}
}
private static string GetDefaultErrorMessage(ValidationAttribute validationAttribute)
{
if (validationAttribute is StringLengthAttribute stringLengthAttribute && stringLengthAttribute.MinimumLength != 0)
{
var nullable = ValidationAttributeHelper.ValidationAttributeCustomErrorMessageSetProperty.GetValue((object) validationAttribute) as bool?;
var flag = true;
if (!(nullable.GetValueOrDefault() == flag & nullable.HasValue))
{
return ValidationAttributeHelper.SetErrorMessagePlaceholders("The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.");
}
}
return ValidationAttributeHelper.SetErrorMessagePlaceholders(ValidationAttributeHelper.ValidationAttributeErrorMessageStringProperty.GetValue((object) validationAttribute) as string);
}
}

@ -2,18 +2,20 @@
@typeparam TResourceType
@using Abp.Localization
@using Blazorise.Components
@inherits ExtensionPropertyComponentBase<TEntity, TResourceType>
<Field>
<FieldLabel>@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)</FieldLabel>
<Autocomplete Data="@lookupItems"
TItem="SelectItem<object>"
TValue="object"
TextField="item=>item.Text"
ValueField="item=>item.Value"
TextField="item => item?.Text"
ValueField="item => item?.Value"
SelectedText="@SelectedText"
SelectedValue="@SelectedValue"
SelectedValueChanged="@SelectedValueChanged"
SearchChanged="@SearchFilterChangedAsync"
Validator="@Validate"
MinLength="0">
</Autocomplete>
</Field>

@ -9,6 +9,8 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using Blazorise;
using Volo.Abp.AspNetCore.Components.Web;
using Volo.Abp.AspNetCore.Components.Web.Extensibility;
using Volo.Abp.Data;
using Volo.Abp.Http;
@ -18,18 +20,11 @@ using Volo.Abp.ObjectExtending;
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending;
public partial class LookupExtensionProperty<TEntity, TResourceType> : ComponentBase
public partial class LookupExtensionProperty<TEntity, TResourceType>
where TEntity : IHasExtraProperties
{
protected List<SelectItem<object>> lookupItems;
[Inject] public IStringLocalizerFactory StringLocalizerFactory { get; set; }
[Parameter] public TEntity Entity { get; set; }
[Parameter] public ObjectExtensionPropertyInfo PropertyInfo { get; set; }
[Inject] public ILookupApiRequestService LookupApiService { get; set; }
public string TextPropertyName => PropertyInfo.Name + "_Text";
@ -42,28 +37,22 @@ public partial class LookupExtensionProperty<TEntity, TResourceType> : Component
}
}
public string SelectedText => Entity.GetProperty<string>(TextPropertyName);
public LookupExtensionProperty()
{
lookupItems = new List<SelectItem<object>>();
}
protected override void OnParametersSet()
protected async override Task OnInitializedAsync()
{
var value = Entity.GetProperty(PropertyInfo.Name);
var text = Entity.GetProperty(TextPropertyName);
if (value != null && text != null)
{
lookupItems.Add(new SelectItem<object>
{
Text = Entity.GetProperty(TextPropertyName).ToString(),
Value = value
});
}
await base.OnInitializedAsync();
await SearchFilterChangedAsync(string.Empty);
}
protected virtual void UpdateLookupTextProperty(object value)
{
var selectedItemText = lookupItems.SingleOrDefault(t => t.Value.Equals(value)).Text;
var selectedItemText = lookupItems.SingleOrDefault(t => t.Value.Equals(value))?.Text;
Entity.SetProperty(TextPropertyName, selectedItemText);
}
@ -86,8 +75,7 @@ public partial class LookupExtensionProperty<TEntity, TResourceType> : Component
selectItems.Add(new SelectItem<object>
{
Text = item.GetProperty(PropertyInfo.Lookup.DisplayPropertyName).GetString(),
Value = JsonSerializer.Deserialize(
item.GetProperty(PropertyInfo.Lookup.ValuePropertyName).GetRawText(), PropertyInfo.Type)
Value = JsonSerializer.Deserialize(item.GetProperty(PropertyInfo.Lookup.ValuePropertyName).GetRawText(), PropertyInfo.Type)
});
}
@ -97,22 +85,11 @@ public partial class LookupExtensionProperty<TEntity, TResourceType> : Component
protected virtual Task SelectedValueChanged(object selectedItem)
{
SelectedValue = selectedItem;
return Task.CompletedTask;
}
protected async Task SearchFilterChangedAsync(string filter)
protected virtual async Task SearchFilterChangedAsync(string filter)
{
lookupItems = await GetLookupItemsAsync(filter);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await SearchFilterChangedAsync(string.Empty);
}
}
}

@ -1,13 +1,21 @@
@typeparam TEntity
@typeparam TResourceType
@using Abp.Localization
@inherits ExtensionPropertyComponentBase<TEntity, TResourceType>
<Field>
<FieldLabel>@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)</FieldLabel>
<Select @bind-SelectedValue="@SelectedValue" >
@foreach (var item in SelectItems)
{
<SelectItem Value="@item.Value">@item.Text</SelectItem>
}
</Select>
</Field>
<Validation Validator="@Validate" MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)</FieldLabel>
<Select @bind-SelectedValue="@SelectedValue">
<ChildContent>
@foreach (var item in SelectItems)
{
<SelectItem Value="@item.Value">@item.Text</SelectItem>
}
</ChildContent>
<Feedback>
<ValidationError/>
</Feedback>
</Select>
</Field>
</Validation>

@ -2,23 +2,20 @@
using Microsoft.Extensions.Localization;
using System;
using System.Collections.Generic;
using System.Linq;
using Blazorise;
using Volo.Abp.AspNetCore.Components.Web;
using Volo.Abp.Data;
using Volo.Abp.Localization;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending;
public partial class SelectExtensionProperty<TEntity, TResourceType> : ComponentBase
public partial class SelectExtensionProperty<TEntity, TResourceType>
where TEntity : IHasExtraProperties
{
protected List<SelectItem<int>> SelectItems = new();
[Inject] public IStringLocalizerFactory StringLocalizerFactory { get; set; }
[Parameter] public TEntity Entity { get; set; }
[Parameter] public ObjectExtensionPropertyInfo PropertyInfo { get; set; }
public int SelectedValue {
get { return Entity.GetProperty<int>(PropertyInfo.Name); }
set { Entity.SetProperty(PropertyInfo.Name, value, false); }

@ -2,13 +2,18 @@
@typeparam TResourceType
@using Volo.Abp.BlazoriseUI
@using Volo.Abp.Localization
@inherits ExtensionPropertyComponentBase<TEntity, TResourceType>
@if (PropertyInfo != null && Entity != null)
{
<Field>
<FieldLabel>@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)</FieldLabel>
<TextEdit @bind-Text="@Value" Role="@PropertyInfo.GetTextRole()" InputMode="@PropertyInfo.GetTextInputMode()">
</TextEdit>
</Field>
}
<Validation Validator="@Validate" MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)</FieldLabel>
<TextEdit @bind-Text="@Value" Role="@PropertyInfo.GetTextRole()" InputMode="@PropertyInfo.GetTextInputMode()">
<Feedback>
<ValidationError/>
</Feedback>
</TextEdit>
</Field>
</Validation>
}

@ -1,23 +1,10 @@
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending;
using Volo.Abp.Data;
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending;
public partial class TextExtensionProperty<TEntity, TResourceType> : ComponentBase
public partial class TextExtensionProperty<TEntity, TResourceType>
where TEntity : IHasExtraProperties
{
[Inject]
public IStringLocalizerFactory StringLocalizerFactory { get; set; }
[Parameter]
public TEntity Entity { get; set; }
[Parameter]
public ObjectExtensionPropertyInfo PropertyInfo { get; set; }
protected string Value {
get {
return PropertyInfo.GetTextInputValueOrNull(Entity.GetProperty(PropertyInfo.Name));

@ -2,12 +2,18 @@
@typeparam TResourceType
@using Volo.Abp.BlazoriseUI
@using Volo.Abp.Localization
@inherits ExtensionPropertyComponentBase<TEntity, TResourceType>
@if (PropertyInfo != null && Entity != null)
{
<Field>
<FieldLabel>@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)</FieldLabel>-->
<TimeEdit TValue="TimeSpan?" @bind-Time="@Value">
</TimeEdit>
</Field>
}
<Validation Validator="@Validate" MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)</FieldLabel>-->
<TimeEdit TValue="TimeSpan?" @bind-Time="@Value">
<Feedback>
<ValidationError/>
</Feedback>
</TimeEdit>
</Field>
</Validation>
}

@ -5,23 +5,16 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Blazorise;
using Volo.Abp.AspNetCore.Components.Web;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending;
public partial class TimeExtensionProperty<TEntity, TResourceType> : ComponentBase
public partial class TimeExtensionProperty<TEntity, TResourceType>
where TEntity : IHasExtraProperties
{
[Inject]
public IStringLocalizerFactory StringLocalizerFactory { get; set; }
[Parameter]
public TEntity Entity { get; set; }
[Parameter]
public ObjectExtensionPropertyInfo PropertyInfo { get; set; }
protected TimeSpan? Value {
get {
return PropertyInfo.GetInputValueOrDefault<TimeSpan?>(Entity.GetProperty(PropertyInfo.Name));

@ -14,10 +14,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Blazorise" Version="0.9.5" />
<PackageReference Include="Blazorise.DataGrid" Version="0.9.5" />
<PackageReference Include="Blazorise.Snackbar" Version="0.9.5" />
<PackageReference Include="Blazorise.Components" Version="0.9.5" />
<PackageReference Include="Blazorise" Version="0.9.5.4" />
<PackageReference Include="Blazorise.DataGrid" Version="0.9.5.4" />
<PackageReference Include="Blazorise.Snackbar" Version="0.9.5.4" />
<PackageReference Include="Blazorise.Components" Version="0.9.5.4" />
</ItemGroup>
</Project>

@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Reflection;
@ -9,7 +9,7 @@ public static class ProxyHelper
private const string ProxyNamespace = "Castle.Proxies";
/// <summary>
/// Returns dynamic proxy target object if this is a proxied object, otherwise returns the given object.
/// Returns dynamic proxy target object if this is a proxied object, otherwise returns the given object.
/// It supports Castle Dynamic Proxies.
/// </summary>
public static object UnProxy(object obj)
@ -33,6 +33,20 @@ public static class ProxyHelper
public static Type GetUnProxiedType(object obj)
{
return UnProxy(obj).GetType();
if (obj.GetType().Namespace == ProxyNamespace)
{
var target = UnProxy(obj);
if (target != null)
{
if (target == obj)
{
return obj.GetType().GetTypeInfo().BaseType;
}
return target.GetType();
}
}
return obj.GetType();
}
}

@ -49,3 +49,51 @@ public class LimitedResultRequestDto : ILimitedResultRequest, IValidatableObject
}
}
}
/// <summary>
/// Simply implements <see cref="ILimitedResultRequest"/>.
/// </summary>
[Serializable]
public class ExtensibleLimitedResultRequestDto : ExtensibleEntityDto, ILimitedResultRequest, IValidatableObject
{
/// <summary>
/// Default value: 10.
/// </summary>
public static int DefaultMaxResultCount { get; set; } = 10;
/// <summary>
/// Maximum possible value of the <see cref="MaxResultCount"/>.
/// Default value: 1,000.
/// </summary>
public static int MaxMaxResultCount { get; set; } = 1000;
/// <summary>
/// Maximum result count should be returned.
/// This is generally used to limit result count on paging.
/// </summary>
[Range(1, int.MaxValue)]
public virtual int MaxResultCount { get; set; } = DefaultMaxResultCount;
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
foreach(var result in base.Validate(validationContext))
{
yield return result;
}
if (MaxResultCount > MaxMaxResultCount)
{
var localizer = validationContext.GetRequiredService<IStringLocalizer<AbpDddApplicationContractsResource>>();
yield return new ValidationResult(
localizer[
"MaxResultCountExceededExceptionMessage",
nameof(MaxResultCount),
MaxMaxResultCount,
typeof(ExtensibleLimitedResultRequestDto).FullName,
nameof(MaxMaxResultCount)
],
new[] { nameof(MaxResultCount) });
}
}
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.Application.Dtos;
@ -7,7 +8,8 @@ namespace Volo.Abp.Application.Dtos;
public class ListResultDto<T> : IListResult<T>
{
/// <inheritdoc />
public IReadOnlyList<T> Items {
public IReadOnlyList<T> Items
{
get { return _items ?? (_items = new List<T>()); }
set { _items = value; }
}
@ -30,3 +32,32 @@ public class ListResultDto<T> : IListResult<T>
Items = items;
}
}
[Serializable]
public class ExtensibleListResultDto<T> : ExtensibleObject, IListResult<T>
{
/// <inheritdoc />
public IReadOnlyList<T> Items
{
get { return _items ?? (_items = new List<T>()); }
set { _items = value; }
}
private IReadOnlyList<T> _items;
/// <summary>
/// Creates a new <see cref="ListResultDto{T}"/> object.
/// </summary>
public ExtensibleListResultDto()
{
}
/// <summary>
/// Creates a new <see cref="ListResultDto{T}"/> object.
/// </summary>
/// <param name="items">List of items</param>
public ExtensibleListResultDto(IReadOnlyList<T> items)
{
Items = items;
}
}

@ -10,3 +10,12 @@ public class PagedAndSortedResultRequestDto : PagedResultRequestDto, IPagedAndSo
{
public virtual string Sorting { get; set; }
}
/// <summary>
/// Simply implements <see cref="IPagedAndSortedResultRequest"/>.
/// </summary>
[Serializable]
public class ExtensiblePagedAndSortedResultRequestDto : ExtensiblePagedResultRequestDto, IPagedAndSortedResultRequest
{
public virtual string Sorting { get; set; }
}

@ -32,3 +32,33 @@ public class PagedResultDto<T> : ListResultDto<T>, IPagedResult<T>
TotalCount = totalCount;
}
}
/// <summary>
/// Implements <see cref="IPagedResult{T}"/>.
/// </summary>
/// <typeparam name="T">Type of the items in the <see cref="ListResultDto{T}.Items"/> list</typeparam>
[Serializable]
public class ExtensiblePagedResultDto<T> : ExtensibleListResultDto<T>, IPagedResult<T>
{
/// <inheritdoc />
public long TotalCount { get; set; } //TODO: Can be a long value..?
/// <summary>
/// Creates a new <see cref="PagedResultDto{T}"/> object.
/// </summary>
public ExtensiblePagedResultDto()
{
}
/// <summary>
/// Creates a new <see cref="PagedResultDto{T}"/> object.
/// </summary>
/// <param name="totalCount">Total count of Items</param>
/// <param name="items">List of items in current page</param>
public ExtensiblePagedResultDto(long totalCount, IReadOnlyList<T> items)
: base(items)
{
TotalCount = totalCount;
}
}

@ -1,4 +1,4 @@
using System;
using System;
using System.ComponentModel.DataAnnotations;
namespace Volo.Abp.Application.Dtos;
@ -12,3 +12,13 @@ public class PagedResultRequestDto : LimitedResultRequestDto, IPagedResultReques
[Range(0, int.MaxValue)]
public virtual int SkipCount { get; set; }
}
/// <summary>
/// Simply implements <see cref="IPagedResultRequest"/>.
/// </summary>
[Serializable]
public class ExtensiblePagedResultRequestDto : ExtensibleLimitedResultRequestDto, IPagedResultRequest
{
[Range(0, int.MaxValue)]
public virtual int SkipCount { get; set; }
}

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -205,14 +205,16 @@ var abp = abp || {};
handleAbpErrorResponse: function (jqXHR, userOptions, $dfd) {
var messagePromise = null;
var responseJSON = jqXHR.responseJSON ? jqXHR.responseJSON : JSON.parse(jqXHR.responseText);
if (userOptions.abpHandleError !== false) {
messagePromise = abp.ajax.showError(jqXHR.responseJSON.error);
messagePromise = abp.ajax.showError(responseJSON.error);
}
abp.ajax.logError(jqXHR.responseJSON.error);
abp.ajax.logError(responseJSON.error);
$dfd && $dfd.reject(jqXHR.responseJSON.error, jqXHR);
userOptions.error && userOptions.error(jqXHR.responseJSON.error, jqXHR);
$dfd && $dfd.reject(responseJSON.error, jqXHR);
userOptions.error && userOptions.error(responseJSON.error, jqXHR);
if (jqXHR.status === 401 && userOptions.abpHandleError !== false) {
abp.ajax.handleUnAuthorizedRequest(messagePromise);

@ -13,8 +13,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazorise.Bootstrap5" Version="0.9.5" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="0.9.5" />
<PackageReference Include="Blazorise.Bootstrap5" Version="0.9.5.4" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="0.9.5.4" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="6.0.0" />

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -13,8 +13,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazorise.Bootstrap5" Version="0.9.5" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="0.9.5" />
<PackageReference Include="Blazorise.Bootstrap5" Version="0.9.5.4" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="0.9.5.4" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
</ItemGroup>

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -8,8 +8,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazorise.Bootstrap5" Version="0.9.5" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="0.9.5" />
<PackageReference Include="Blazorise.Bootstrap5" Version="0.9.5.4" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="0.9.5.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" />
</ItemGroup>

File diff suppressed because one or more lines are too long

@ -8,7 +8,7 @@
<base href="/" />
<!--ABP:Styles-->
<link href="global.css?_v=637728566120563110" rel="stylesheet"/>
<link href="global.css?_v=637770774125431863" rel="stylesheet"/>
<link href="main.css" rel="stylesheet"/>
<!--/ABP:Styles-->
<link href="MyCompanyName.MyProjectName.Blazor.styles.css" rel="stylesheet"/>
@ -23,7 +23,7 @@
</div>
<!--ABP:Scripts-->
<script src="global.js?_v=637728566121956479"></script>
<script src="global.js?_v=637770774126750116"></script>
<!--/ABP:Scripts-->
</body>
</html>

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -27,7 +27,7 @@ public class MyProjectNameWebTestModule : AbpModule
{
context.Services.PreConfigure<IMvcBuilder>(builder =>
{
builder.PartManager.ApplicationParts.Add(new AssemblyPart(typeof(MyProjectNameWebModule).Assembly));
builder.PartManager.ApplicationParts.Add(new CompiledRazorAssemblyPart(typeof(MyProjectNameWebModule).Assembly));
});
}

@ -8,8 +8,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazorise.Bootstrap5" Version="0.9.5" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="0.9.5" />
<PackageReference Include="Blazorise.Bootstrap5" Version="0.9.5.4" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="0.9.5.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" />
</ItemGroup>

@ -8,7 +8,7 @@
<base href="/" />
<!--ABP:Styles-->
<link href="global.css?_v=637728566152161888" rel="stylesheet"/>
<link href="global.css?_v=637770774266638185" rel="stylesheet"/>
<link href="main.css" rel="stylesheet"/>
<!--/ABP:Styles-->
</head>
@ -22,7 +22,7 @@
</div>
<!--ABP:Scripts-->
<script src="global.js?_v=637728566153545942"></script>
<script src="global.js?_v=637770774268198448"></script>
<!--/ABP:Scripts-->
</body>
</html>

@ -12,8 +12,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazorise.Bootstrap5" Version="0.9.5" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="0.9.5" />
<PackageReference Include="Blazorise.Bootstrap5" Version="0.9.5.4" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="0.9.5.4" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0" />

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();

@ -0,0 +1,5 @@
(function () {
[].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl)
})
})();
Loading…
Cancel
Save