diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE index d200c54488..414985237d 100644 --- a/.github/ISSUE_TEMPLATE +++ b/.github/ISSUE_TEMPLATE @@ -10,6 +10,7 @@ If you're creating a bug/problem report, please include followings: * Your **ABP Framework version**. * Your **User Interface** type (Angular/MVC/React... etc.) if the issue is related to a specific UI +* Your database provider(EF Core/MongoDB) * Exception message and **stack trace** if available (check the logs). * Steps needed to **reproduce** the problem. @@ -21,4 +22,4 @@ Please use Stack Overflow for your questions about using the framework, template https://stackoverflow.com/questions/tagged/abp -Use **abp** tag in your questions. \ No newline at end of file +Use **abp** tag in your questions. diff --git a/.github/workflows/auto-pr.yml b/.github/workflows/auto-pr.yml new file mode 100644 index 0000000000..be8ad519d4 --- /dev/null +++ b/.github/workflows/auto-pr.yml @@ -0,0 +1,24 @@ +name: Merge branch dev with rel-4.2 +on: + push: + branches: + - rel-4.2 +jobs: + merge-dev-with-rel-4-2: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: dev + - name: Reset promotion branch + run: | + git fetch origin rel-4.2:rel-4.2 + git reset --hard rel-4.2 + - name: Create Pull Request + uses: peter-evans/create-pull-request@v3 + with: + branch: auto-merge/rel-4-2/${{github.run_number}} + title: Merge branch dev with rel-4.2 + body: This PR generated automatically to merge dev with rel-4.2. Please review the changed files before merging to prevent any errors that may occur. + reviewers: ${{github.actor}} + token: ${{ github.token }} diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 062bf71d89..6ba3ad7a6d 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-dotnet@master with: - dotnet-version: 5.0.100-rc.2.20479.15 + dotnet-version: 5.0.100 - name: Build All run: .\build-all.ps1 diff --git a/.gitignore b/.gitignore index 5b8dfe9c11..407241c7fc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.user *.userosscache *.sln.docstates +*.editorconfig # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -152,6 +153,7 @@ PublishScripts/ # NuGet Packages *.nupkg +*.snupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000000..d888f2a60a --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,29 @@ + + + + + 5.0.* + + + 16.8.3 + + + 4.2.2 + + + 4.0.1 + + + 2.4.1 + + + 2.4.1 + + + 2.4.3 + + + 2.2.14 + + + \ No newline at end of file diff --git a/README.md b/README.md index 9dffe69012..a73e9ef3ed 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,106 @@ ![build and test](https://github.com/abpframework/abp/workflows/build%20and%20test/badge.svg) [![NuGet](https://img.shields.io/nuget/v/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) -[![MyGet (with prereleases)](https://img.shields.io/myget/abp-nightly/vpre/Volo.Abp.svg?style=flat-square)](https://docs.abp.io/en/abp/latest/Nightly-Builds) +[![NuGet (with prereleases)](https://img.shields.io/nuget/vpre/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) +[![MyGet (nightly builds)](https://img.shields.io/myget/abp-nightly/vpre/Volo.Abp.svg?style=flat-square)](https://docs.abp.io/en/abp/latest/Nightly-Builds) [![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) -ABP is an **open source application framework** focused on ASP.NET Core based web application development, but also supports developing other type of applications. +ABP Framework is a complete **infrastructure** based on the **ASP.NET Core** to create **modern web applications** and **APIs** by following the software development **best practices** and the **latest technologies**. -## Links +## Getting Started -* Official Web Site +- [Getting Started Guide](https://docs.abp.io/en/abp/latest/Getting-Started) is the easiest way to start a new web application with the ABP Framework. +- [Web Application Development Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1) is a complete tutorial to develop a full stack web application. + +### Quick Start + +Install the ABP CLI: + +````bash +> dotnet tool install -g Volo.Abp.Cli +```` + +Create a new solution: + +````bash +> abp new BookStore -u mvc -d ef +```` + +> See the [CLI documentation](https://docs.abp.io/en/abp/latest/CLI) for all available options. + +### UI Framework Options + + + +### Database Provider Options + + + +## What ABP Provides? + +ABP provides a **full stack developer experience**. + +### Architecture + + + +ABP offers a complete, **modular** and **layered** software architecture based on **[Domain Driven Design](https://docs.abp.io/en/abp/latest/Domain-Driven-Design)** principles and patterns. It also provides the necessary infrastructure and guiding to [implement this architecture](https://docs.abp.io/en/abp/latest/Domain-Driven-Design-Implementation-Guide). + +ABP Framework is suitable for **[microservice solutions](https://docs.abp.io/en/abp/latest/Microservice-Architecture)** as well as monolithic applications. + +### Infrastructure + +There are a lot of features provided by the ABP Framework to achieve real world scenarios easier, like [Event Bus](https://docs.abp.io/en/abp/latest/Event-Bus), [Background Job System](https://docs.abp.io/en/abp/latest/Background-Jobs), [Audit Logging](https://docs.abp.io/en/abp/latest/Audit-Logging), [BLOB Storing](https://docs.abp.io/en/abp/latest/Blob-Storing), [Data Seeding](https://docs.abp.io/en/abp/latest/Data-Seeding), [Data Filtering](https://docs.abp.io/en/abp/latest/Data-Filtering), etc. + +### Cross Cutting Concerns + +ABP also simplifies (and even automates wherever possible) cross cutting concerns and common non-functional requirements like [Exception Handling](https://docs.abp.io/en/abp/latest/Exception-Handling), [Validation](https://docs.abp.io/en/abp/latest/Validation), [Authorization](https://docs.abp.io/en/abp/latest/Authorization), [Localization](https://docs.abp.io/en/abp/latest/Localization), [Caching](https://docs.abp.io/en/abp/latest/Caching), [Dependency Injection](https://docs.abp.io/en/abp/latest/Dependency-Injection), [Setting Management](https://docs.abp.io/en/abp/latest/Settings), etc. + +### Application Modules + +ABP is a modular framework and the Application Modules provide **pre-built application functionalities**; + +- [**Account**](https://docs.abp.io/en/abp/latest/Modules/Account): Provides UI for the account management and allows user to login/register to the application. +- **[Identity](https://docs.abp.io/en/abp/latest/Modules/Identity)**: Manages organization units, roles, users and their permissions, based on the Microsoft Identity library. +- [**IdentityServer**](https://docs.abp.io/en/abp/latest/Modules/IdentityServer): Integrates to IdentityServer4. +- [**Tenant Management**](https://docs.abp.io/en/abp/latest/Modules/Tenant-Management): Manages tenants for a [multi-tenant](https://docs.abp.io/en/abp/latest/Multi-Tenancy) (SaaS) application. + +See the [Application Modules](https://docs.abp.io/en/abp/latest/Modules/Index) document for all pre-built modules. + +### Startup Templates + +The [Startup templates](https://docs.abp.io/en/abp/latest/Startup-Templates/Index) are pre-built Visual Studio solution templates. You can create your own solution based on these templates to **immediately start your development**. + +## ABP Community + +### ABP Community Web Site + +The [ABP Community](https://community.abp.io/) is a website to publish **articles** and share **knowledge** about the ABP Framework. You can also create content for the community! + +### Blog + +Follow the [ABP Blog](https://blog.abp.io/) to learn the latest happenings in the ABP Framework. + +### Samples + +See the [sample projects](https://docs.abp.io/en/abp/latest/Samples/Index) built with the ABP Framework. + +### Want to Contribute? + +ABP is a community-driven open source project. See [the contribution guide](https://docs.abp.io/en/abp/latest/Contribution/Index) if you want to be a part of this project. + +## Official Links + +* Main Web Site * Get Started * Features - * Documentation - * Samples - * Blog +* Documentation +* Samples +* Blog +* Community * Stack overflow * Twitter -## Contribution +## Support the ABP Framework -ABP is an open source platform. Check [the contribution guide](docs/en/Contribution/Index.md) if you want to contribute to the project. +Love ABP Framework? **Please give a star** to this repository :star: diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/de-DE.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/de-DE.json new file mode 100644 index 0000000000..b0418f082f --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/de-DE.json @@ -0,0 +1,14 @@ +{ + "culture": "de-DE", + "texts": { + "Account": "ABP Benutzerkonto - Anmeldung & Registrierung | ABP.IO", + "Welcome": "Willkommen", + "UseOneOfTheFollowingLinksToContinue": "Nutzen Sie einen der nachfolgenden Links um fortzusetzen", + "FrameworkHomePage": "Framework Website", + "FrameworkDocumentation": "Framework Dokumentation", + "OfficialBlog": "Offizieller Blog", + "CommercialHomePage": "Commercial Website", + "CommercialSupportWebSite": "Commercial Support-Website", + "CommunityWebSite": "ABP Community-Website" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en-GB.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en-GB.json new file mode 100644 index 0000000000..c1f56de865 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en-GB.json @@ -0,0 +1,14 @@ +{ + "culture": "en-GB", + "texts": { + "Account": "ABP Account - Login & Register | ABP.IO", + "Welcome": "Welcome", + "UseOneOfTheFollowingLinksToContinue": "Use one of the following links to continue", + "FrameworkHomePage": "Framework home page", + "FrameworkDocumentation": "Framework documentation", + "OfficialBlog": "Official blog", + "CommercialHomePage": "Commercial home page", + "CommercialSupportWebSite": "Commercial support web site", + "CommunityWebSite": "ABP community web site" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/es.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/es.json new file mode 100644 index 0000000000..44ec873da0 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/es.json @@ -0,0 +1,14 @@ +{ + "culture": "es", + "texts": { + "Account": "Cuenta de ABP - Iniciar sesión y registrarse | ABP.IO", + "Welcome": "Bienvenido", + "UseOneOfTheFollowingLinksToContinue": "Usa uno de los siguientes links para continuar", + "FrameworkHomePage": "Página de inicio del framework", + "FrameworkDocumentation": "Documentación del framework", + "OfficialBlog": "Blog Oficial", + "CommercialHomePage": "Página de inicio comercial", + "CommercialSupportWebSite": "Sitio web de soporte comercial", + "CommunityWebSite": "Sitio web comunidad ABP" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/de-DE.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/de-DE.json new file mode 100644 index 0000000000..60725c437b --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/de-DE.json @@ -0,0 +1,199 @@ +{ + "culture": "de-DE", + "texts": { + "Permission:Organizations": "Organisationen", + "Permission:Manage": "Organisationen verwalten", + "Permission:DiscountRequests": "Rabattanfragen", + "Permission:DiscountManage": "Rabattanfragen verwalten", + "Permission:Disable": "Deaktivieren", + "Permission:Enable": "Aktivieren", + "Permission:EnableSendEmail": "E-Mail-Senden aktivieren", + "Permission:SendEmail": "E-Mail senden", + "Permission:NpmPackages": "NPM-Pakete", + "Permission:NugetPackages": "Nuget-Pakete", + "Permission:Maintenance": "Wartung", + "Permission:Maintain": "Warten", + "Permission:ClearCaches": "Caches leeren", + "Permission:Modules": "Module", + "Permission:Packages": "Pakete", + "Permission:Edit": "Bearbeiten", + "Permission:Delete": "Löschen", + "Permission:Create": "Erstellen", + "Permission:Accounting": "Abrechnung", + "Permission:Accounting:Quotation": "Angebot", + "Permission:Accounting:Invoice": "Rechnung", + "Menu:Organizations": "Organisationen", + "Menu:Accounting": "Abrechnung", + "Menu:Packages": "Pakete", + "Menu:DiscountRequests": "Rabattanfragen", + "NpmPackageDeletionWarningMessage": "Dieses NPM-Paket wird entfernt. Bestätigen Sie das?", + "NugetPackageDeletionWarningMessage": "Dieses Nuget-Paket wird entfernt. Bestägiten Sie das?", + "ModuleDeletionWarningMessage": "Dieses Modul wird entfernt. Bestätigen Sie das?", + "Name": "Name", + "DisplayName": "Anzeigename", + "ShortDescription": "Kurzbeschreibung", + "NameFilter": "Name", + "CreationTime": "Erstellungszeitpunkt", + "IsPro": "Ist pro", + "ShowOnModuleList": "In Modulliste anzeigen", + "EfCoreConfigureMethodName": "Methodenname konfigurieren", + "IsProFilter": "Ist pro", + "ApplicationType": "Anwendungstyp", + "Target": "Ziel", + "TargetFilter": "Ziel", + "ModuleClass": "Modulklasse", + "NugetPackageTarget.DomainShared": "Gemeinsame Domain", + "NugetPackageTarget.Domain": "Domain", + "NugetPackageTarget.Application": "Anwendung", + "NugetPackageTarget.ApplicationContracts": "Anwedungsverträge", + "NugetPackageTarget.HttpApi": "HTTP-API", + "NugetPackageTarget.HttpApiClient": "HTTP-API-Client", + "NugetPackageTarget.Web": "Web", + "NugetPackageTarget.EntityFrameworkCore": "DeleteAllEntityFramework Core", + "NugetPackageTarget.MongoDB": "MongoDB", + "Edit": "Bearbeiten", + "Delete": "Löschen", + "Refresh": "Aktualisieren", + "NpmPackages": "NPM-Pakete", + "NugetPackages": "Nuget-Pakete", + "NpmPackageCount": "NPM-Paketanzahl", + "NugetPackageCount": "Nuget-Paketanzahl", + "Module": "Module", + "ModuleInfo": "Modulinfo", + "CreateANpmPackage": "Erstellen Sie ein NPM Paket", + "CreateAModule": "Erstellen Sie in Modul", + "CreateANugetPackage": "Erstellen Sei ein Nuget-Paket", + "AddNew": "Neu hinzufügen", + "PackageAlreadyExist{0}": "\"{0}\" Paket ist bereits hinzugefügt.", + "ModuleAlreadyExist{0}": "\"{0}\" Modul ist bereits hinzugefügt.", + "ClearCache": "Cache leeren", + "SuccessfullyCleared": "Erfolgreich geleert", + "Menu:NpmPackages": "NPM-Pakete", + "Menu:Modules": "Module", + "Menu:Maintenance": "Wartung", + "Menu:NugetPackages": "Nuget-Pakete", + "CreateAnOrganization": "Erstellen Sie eine Organisation", + "Organizations": "Organisationen", + "LongName": "Lange Name", + "LicenseType": "Lizenztyp", + "MissingLicenseTypeField": "Das Feld Lizenztyp ist erforderlich!", + "LicenseStartTime": "Startzeit der Lizenz", + "LicenseEndTime": "Endzeit der Lizenz", + "AllowedDeveloperCount": "Zulässige Entwickleranzahl", + "UserNameOrEmailAddress": "Benutzername oder E-Mail-Adresse", + "AddOwner": "Besitzer hinzufügen", + "UserName": "Benutzername", + "Email": "E-Mail", + "Developers": "Entwickler", + "AddDeveloper": "Entwickler hinzufügen", + "Create": "Erstellen", + "UserNotFound": "Benutzer nicht gefunden", + "{0}WillBeRemovedFromDevelopers": "{0} wird von den Entwicklern entfernt. Bestätigen Sie das?", + "{0}WillBeRemovedFromOwners": "{0} wird von den Besitzern entfernt. Bestätigen Sie das?", + "Computers": "Computer", + "UniqueComputerId": "Eindeutig Computer-ID", + "LastSeenDate": "Zuletzt gesehenes Datum", + "{0}Computer{1}WillBeRemovedFromRecords": "Computer von {0} ({1}) wird aus den Datensätzen entfernt", + "OrganizationDeletionWarningMessage": "Organisation wird gelöscht", + "DeletingLastOwnerWarningMessage": "Eine Organisation muss zumindest einen Besitzer aufweisen! Daher können Sie diesen Besitzer nicht entfernen", + "This{0}AlreadyExistInThisOrganization": "Dies {0} existiert bereits in dieser Organisation", + "AreYouSureYouWantToDeleteAllComputers": "Sind Sie sicher, dass Sie alle Computer löschen möchten?", + "DeleteAll": "Alles Löschen", + "DoYouWantToCreateNewUser": "Möchten Sie einen neuen Benutzer erstellen?", + "MasterModules": "Master-Module", + "OrganizationName": "Organisationsname", + "CreationDate": "Erstellungsdatum", + "LicenseStartDate": "Startdatum der Lizenz", + "LicenseEndDate": "Enddatum der Lizenz", + "OrganizationNamePlaceholder": "Organisationsname...", + "TotalQuestionCountPlaceholder": "Gesamtzahl der Fragen...", + "RemainingQuestionCountPlaceholder": "Anzahl verbleibender Fragen...", + "LicenseTypePlaceholder": "Lizenztyp...", + "CreationDatePlaceholder": "Erstellungsdatum...", + "LicenseStartDatePlaceholder": "Startdatum der Lizenz...", + "LicenseEndDatePlaceholder": "Enddatum der Lizenz...", + "UsernameOrEmail": "Benutzername oder E-Mail-Adresse", + "UsernameOrEmailPlaceholder": "Benutzername oder E-Mail-Adresse...", + "Member": "Mitglied", + "PurchaseOrderNo": "Bestellnummer", + "QuotationDate": "Angebotsdatum", + "CompanyName": "Firmenname", + "CompanyAddress": "Firmenanschrift", + "Price": "Preis", + "DiscountText": "Rabatttext", + "DiscountQuantity": "Rabattmenge", + "DiscountPrice": "Rabattpreis", + "Quotation": "Angebot", + "ExtraText": "Zusätzlicher Text", + "ExtraAmount": "Zusätzliche Menge", + "DownloadQuotation": "Angebot herunterladen", + "Invoice": "Rechnung", + "TaxNumber": "Steuernummer", + "InvoiceNumber": "Rechnungsnummer", + "InvoiceDate": "Rechnungsdatum", + "InvoiceNote": "Rechnungsnotiz", + "Quantity": "Menge", + "AddProduct": "Produkt hinzufügen", + "AddProductWarning": "Sie müssen ein Produkt hinzufügen!", + "TotalPrice": "Gesamtpreis", + "Generate": "Generieren", + "MissingQuantityField": "Das Feld Menge ist erforderlich!", + "MissingPriceField": "Das Feld Preis ist erforderlich!", + "CodeUsageStatus": "Status", + "Country": "Land", + "DeveloperCount": "Entwickleranzahl", + "RequestCode": "Anfrage-Code", + "WebSite": "Webseite", + "GithubUsername": "Github Benutzername", + "PhoneNumber": "Telefonnummer", + "ProjectDescription": "Projektbeschreibung", + "Referrer": "Referrer", + "DiscountRequests": "Rabattanfrage", + "Copylink": "Link kopieren", + "Disable": "Deaktivieren", + "Enable": "Aktivieren", + "EnableSendEmail": "E-Mail-Senden aktivieren", + "SendEmail": "E-Mail senden", + "SuccessfullyDisabled": "Erfolgreich deaktiviert", + "SuccessfullyEnabled": "Erfolgreich aktiviert", + "EmailSent": "E-Mail gesendet", + "SuccessfullySent": "Erfolgreich gesendet", + "SuccessfullyDeleted": "Erfolgreich gelöscht", + "DiscountRequestDeletionWarningMessage": "Rabattanfrage wird gelöscht", + "BusinessType": "Unternehmensart", + "TotalQuestionCount": "Gesamtzahl der Fragen", + "RemainingQuestionCount": "Anzahl verbleibender Fragen", + "TotalQuestionMustBeGreaterWarningMessage": "TotalQuestionCount muss größer sein als RemainingQuestionCount !", + "QuestionCountsMustBeGreaterThanZero": "TotalQuestionCount und RemainingQuestionCount müssen Null oder größer als Null sein !", + "UnlimitedQuestionCount": "Unbegrenzte Anzahl von Fragen", + "Notes": "Anmerkungen", + "Menu:Community": "Community", + "Menu:Articles": "Beiträge", + "Wait": "Warten", + "Approve": "Genehmigen", + "Reject": "Ablehnen", + "Details": "Details", + "Url": "URL", + "Title": "Titel", + "ContentSource": "Inhaltsquelle", + "Status": "Status", + "ReadArticle": "Beitrag lesen", + "ArticleHasBeenWaiting": "Beitrag hat gewartet", + "ArticleHasBeenApproved": "Beitrag wurde genehmigt", + "ArticleHasBeenRejected": "Beitrag wurde abgelehnt", + "Permission:Community": "Community", + "Permission:CommunityArticle": "Beitrag", + "Link": "Link", + "Enum:ContentSource:0": "Github", + "Enum:ContentSource:1": "Extern", + "Enum:Status:0": "Wartend", + "Enum:Status:1": "Abgelehnt", + "Enum:Status:2": "Genehmigt", + "Summary": "Zusammenfassung", + "AuthorName": "Autorenname", + "CoverImage": "Titelbild", + "RemoveCacheConfirmationMessage": "Sind Sie sicher, dass Sie den Cache für den Artikel \"{0}\" entfernen wollen?", + "SuccessfullyRemoved": "Erfolgreich geleert", + "RemoveCache": "Cache entfernen" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en-GB.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en-GB.json new file mode 100644 index 0000000000..970e04d24e --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en-GB.json @@ -0,0 +1,202 @@ +{ + "culture": "en-GB", + "texts": { + "Permission:Organizations": "Organisations", + "Permission:Manage": "Manage Organisations", + "Permission:DiscountRequests": "Discount Requests", + "Permission:DiscountManage": "Manage Discount Requests", + "Permission:Disable": "Disable", + "Permission:Enable": "Enable", + "Permission:EnableSendEmail": "Enable Send Email", + "Permission:SendEmail": "Send Email", + "Permission:NpmPackages": "NPM Packages", + "Permission:NugetPackages": "Nuget Packages", + "Permission:Maintenance": "Maintenance", + "Permission:Maintain": "Maintain", + "Permission:ClearCaches": "Clear caches", + "Permission:Modules": "Modules", + "Permission:Packages": "Packages", + "Permission:Edit": "Edit", + "Permission:Delete": "Delete", + "Permission:Create": "Create", + "Permission:Accounting": "Accounting", + "Permission:Accounting:Quotation": "Quotation", + "Permission:Accounting:Invoice": "Invoice", + "Menu:Organizations": "Organisations", + "Menu:Accounting": "Accounting", + "Menu:Packages": "Packages", + "Menu:DiscountRequests": "Discount Requests", + "NpmPackageDeletionWarningMessage": "This NPM Package will be deleted. Please confirm this?", + "NugetPackageDeletionWarningMessage": "This Nuget Package will be deleted. Please confirm this?", + "ModuleDeletionWarningMessage": "This Module will be deleted. Please confirm this?", + "Name": "Name", + "DisplayName": "Display name", + "ShortDescription": "Short description", + "NameFilter": "Name", + "CreationTime": "Creation time", + "IsPro": "Is pro", + "ShowOnModuleList": "Show in module list", + "EfCoreConfigureMethodName": "Configure method name", + "IsProFilter": "Is pro", + "ApplicationType": "Application type", + "Target": "Target", + "TargetFilter": "Target", + "ModuleClass": "Module class", + "NugetPackageTarget.DomainShared": "Domain Shared", + "NugetPackageTarget.Domain": "Domain", + "NugetPackageTarget.Application": "Application", + "NugetPackageTarget.ApplicationContracts": "Application Contracts", + "NugetPackageTarget.HttpApi": "Http Api", + "NugetPackageTarget.HttpApiClient": "Http Api Client", + "NugetPackageTarget.Web": "Web", + "NugetPackageTarget.EntityFrameworkCore": "Delete Entity Framework Core", + "NugetPackageTarget.MongoDB": "MongoDB", + "Edit": "Edit", + "Delete": "Delete", + "Refresh": "Refresh", + "NpmPackages": "NPM Packages", + "NugetPackages": "Nuget Packages", + "NpmPackageCount": "NPM Package Count", + "NugetPackageCount": "Nuget Package Count", + "Module": "Modules", + "ModuleInfo": "Module info", + "CreateANpmPackage": "Create a NPM package", + "CreateAModule": "Create a module", + "CreateANugetPackage": "Create a Nuget package", + "AddNew": "Add new", + "PackageAlreadyExist{0}": "\"{0}\" package has already been added.", + "ModuleAlreadyExist{0}": "\"{0}\" module has already been added.", + "ClearCache": "Clear cache", + "SuccessfullyCleared": "Successfully cleared", + "Menu:NpmPackages": "NPM Packages", + "Menu:Modules": "Modules", + "Menu:Maintenance": "Maintenance", + "Menu:NugetPackages": "Nuget Packages", + "CreateAnOrganization": "Create an organisation", + "Organizations": "Organisations", + "LongName": "Long name", + "LicenseType": "License type", + "MissingLicenseTypeField": "The license type field is required!", + "LicenseStartTime": "License start time", + "LicenseEndTime": "License end time", + "AllowedDeveloperCount": "Allowed developer count", + "UserNameOrEmailAddress": "Username or email address", + "AddOwner": "Add owner", + "UserName": "Username", + "Email": "Email", + "Developers": "Developers", + "AddDeveloper": "Add developer", + "Create": "Create", + "UserNotFound": "User not found", + "{0}WillBeRemovedFromDevelopers": "{0} Will be removed from developers, please confirm?", + "{0}WillBeRemovedFromOwners": "{0} Will be removed from owners, please confirm?", + "Computers": "Computers", + "UniqueComputerId": "Unique computer id", + "LastSeenDate": "Last seen date", + "{0}Computer{1}WillBeRemovedFromRecords": "Computer of {0} ({1}) will be removed from records", + "OrganizationDeletionWarningMessage": "Organisation will be deleted", + "DeletingLastOwnerWarningMessage": "An organisation must have at least one owner, therefore you cannot remove this owner", + "This{0}AlreadyExistInThisOrganization": "{0} already exist in this organisation", + "AreYouSureYouWantToDeleteAllComputers": "Are you sure you want to delete all computers?", + "DeleteAll": "Delete all", + "DoYouWantToCreateNewUser": "Do you want to create new user?", + "MasterModules": "Master Modules", + "OrganizationName": "Organisation name", + "CreationDate": "Creation date", + "LicenseStartDate": "License start date", + "LicenseEndDate": "License end date", + "OrganizationNamePlaceholder": "Organisation name...", + "TotalQuestionCountPlaceholder": "Total question count...", + "RemainingQuestionCountPlaceholder": "Remaining question count...", + "LicenseTypePlaceholder": "License type...", + "CreationDatePlaceholder": "Creation date...", + "LicenseStartDatePlaceholder": "License start date...", + "LicenseEndDatePlaceholder": "License end date...", + "UsernameOrEmail": "Username or email", + "UsernameOrEmailPlaceholder": "Username or email...", + "Member": "Member", + "PurchaseOrderNo": "Purchase Order No.", + "QuotationDate": "Quotation date", + "CompanyName": "Company name", + "CompanyAddress": "Company address", + "Price": "Price", + "DiscountText": "Discount text", + "DiscountQuantity": "Discount quantity", + "DiscountPrice": "Discount price", + "Quotation": "Quotation", + "ExtraText": "Extra text", + "ExtraAmount": "Extra Amount", + "DownloadQuotation": "Download Quotation", + "Invoice": "Invoice", + "TaxNumber": "Tax No.", + "InvoiceNumber": "Invoice No.", + "InvoiceDate": "Invoice Date", + "InvoiceNote": "Invoice Note", + "Quantity": "Quantity", + "AddProduct": "Add Product", + "AddProductWarning": "You need to add a Product!", + "TotalPrice": "Total Price", + "Generate": "Generate", + "MissingQuantityField": "The Quantity field is required!", + "MissingPriceField": "The Price field is required!", + "CodeUsageStatus": "Status", + "Country": "Country", + "DeveloperCount": "Developer Count", + "RequestCode": "Request Code", + "WebSite": "Website", + "GithubUsername": "Github Username", + "PhoneNumber": "Phone Number", + "ProjectDescription": "Project Description", + "Referrer": "Referrer", + "DiscountRequests": "Discount Request", + "Copylink": "Copy Link", + "Disable": "Disable", + "Enable": "Enable", + "EnableSendEmail": "Enable Send Email", + "SendEmail": "Send Email", + "SuccessfullyDisabled": "Successfully disabled", + "SuccessfullyEnabled": "Successfully enabled", + "EmailSent": "Email sent", + "SuccessfullySent": "Successfully sent", + "SuccessfullyDeleted": "Successfully deleted", + "DiscountRequestDeletionWarningMessage": "Discount request will be deleted", + "BusinessType": "Business Type", + "TotalQuestionCount": "Total question count", + "RemainingQuestionCount": "Remaining question count", + "TotalQuestionMustBeGreaterWarningMessage": "TotalQuestionCount must be greater than RemainingQuestionCount!", + "QuestionCountsMustBeGreaterThanZero": "TotalQuestionCount and RemainingQuestionCount must be zero or greater than zero!", + "UnlimitedQuestionCount": "Unlimited question count", + "Notes": "Notes", + "Menu:Community": "Community", + "Menu:Articles": "Articles", + "Wait": "Wait", + "Approve": "Approve", + "Reject": "Reject", + "Details": "Details", + "Url": "Url", + "Title": "Title", + "ContentSource": "Content source", + "Status": "Status", + "ReadArticle": "Read article", + "ArticleHasBeenWaiting": "Article has been waiting", + "ArticleHasBeenApproved": "Article has been approved", + "ArticleHasBeenRejected": "Article has been rejected", + "Permission:Community": "Community", + "Permission:CommunityArticle": "Article", + "Link": "Link", + "Enum:ContentSource:0": "Github", + "Enum:ContentSource:1": "External", + "Enum:Status:0": "Waiting", + "Enum:Status:1": "Author name", + "Enum:Status:2": "Approved", + "Summary": "Summary", + "AuthorName": "Author", + "CoverImage": "Cover Image", + "RemoveCacheConfirmationMessage": "Are you sure you want remove the cache for \"{0}\" article?", + "SuccessfullyRemoved": "Successfully cleared", + "RemoveCache": "Remove Cache", + "Language": "Language", + "Optional": "Optional", + "CreateArticleLanguageInfo": "The language in which the article is written" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 6e3c1df3ce..cf6b6a124c 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -194,6 +194,14 @@ "CoverImage": "Cover Image", "RemoveCacheConfirmationMessage": "Are you sure you remove the cache for \"{0}\" article?", "SuccessfullyRemoved": "Successfully cleared", - "RemoveCache": "Remove Cache" + "RemoveCache": "Remove Cache", + "Language": "Language", + "Optional": "Optional", + "CreateArticleLanguageInfo": "The language in which the post is written", + "Enum:ContentSource:2": "Video Post", + "DeleteCoverImage": "Delete Cover Image", + "DeleteCoverImageConfirmationMessage": "Are you sure you want to delete the cover image for \"{0}\"?", + "DeleteCoverImageSuccessMessage": "Cover image successfully deleted" + } -} \ No newline at end of file +} diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/es.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/es.json new file mode 100644 index 0000000000..89ef8706e0 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/es.json @@ -0,0 +1,199 @@ +{ + "culture": "es", + "texts": { + "Permission:Organizations": "Organizaciones", + "Permission:Manage": "Gestionar organizaciones", + "Permission:DiscountRequests": "Solicitudes de descuento", + "Permission:DiscountManage": "Gestionar solicitudes de descuento", + "Permission:Disable": "Desactivar", + "Permission:Enable": "Activar", + "Permission:EnableSendEmail": "Activar enviar email", + "Permission:SendEmail": "Enviar email", + "Permission:NpmPackages": "Paquetes NPM", + "Permission:NugetPackages": "Paquetes Nuget", + "Permission:Maintenance": "Mantenimiento", + "Permission:Maintain": "Mantener", + "Permission:ClearCaches": "Borrar cachés", + "Permission:Modules": "Módulos", + "Permission:Packages": "Paquetes", + "Permission:Edit": "Editar", + "Permission:Delete": "Borrar", + "Permission:Create": "Crear", + "Permission:Accounting": "Contabilidad", + "Permission:Accounting:Quotation": "Cotización", + "Permission:Accounting:Invoice": "Factura", + "Menu:Organizations": "Organizaciones", + "Menu:Accounting": "Contabilidad", + "Menu:Packages": "Paquetes", + "Menu:DiscountRequests": "Solicitudes de descuento", + "NpmPackageDeletionWarningMessage": "Este NPM paquete será borrado. ¿Quieres confirmar?", + "NugetPackageDeletionWarningMessage": "Este NPM paquete será borrado. ¿Quieres confirmar?", + "ModuleDeletionWarningMessage": "Este NPM paquete será borrado. ¿Quieres confirmar?", + "Name": "Nombre", + "DisplayName": "Nombre para mostrar", + "ShortDescription": "Descripción corta", + "NameFilter": "Nombre", + "CreationTime": "Fecha de creación", + "IsPro": "Es pro", + "ShowOnModuleList": "Mostrar en la lista de módulos", + "EfCoreConfigureMethodName": "configurar nombre de método", + "IsProFilter": "Es pro", + "ApplicationType": "tipo de aplicación", + "Target": "Destino", + "TargetFilter": "Destino", + "ModuleClass": "Módulo de clase", + "NugetPackageTarget.DomainShared": "Dominio compartido", + "NugetPackageTarget.Domain": "Dominio", + "NugetPackageTarget.Application": "Aplicación", + "NugetPackageTarget.ApplicationContracts": "Contratos de aplicación", + "NugetPackageTarget.HttpApi": "Http Api", + "NugetPackageTarget.HttpApiClient": "Cliente Http Api", + "NugetPackageTarget.Web": "Web", + "NugetPackageTarget.EntityFrameworkCore": "Delete todo EntityFramework Core", + "NugetPackageTarget.MongoDB": "MongoDB", + "Edit": "Editar", + "Delete": "Borrar", + "Refresh": "Refrescar", + "NpmPackages": "Paquetes NPM", + "NugetPackages": "Paquetes Nuget", + "NpmPackageCount": "Número de paquetes NPM", + "NugetPackageCount": "Número de paquetes Nuget", + "Module": "Módulos", + "ModuleInfo": "Info de módulo", + "CreateANpmPackage": "Crear un paquete NPM", + "CreateAModule": "Crear un módulo", + "CreateANugetPackage": "Crear un paquete de Nuget", + "AddNew": "Añadir nuevo", + "PackageAlreadyExist{0}": "\"{0}\" paquete ya se encuentra añadido", + "ModuleAlreadyExist{0}": "\"{0}\" módulo ya se encuentra añadido.", + "ClearCache": "Borrar caché", + "SuccessfullyCleared": "Borrado satisfactoriamente", + "Menu:NpmPackages": "Paquetes de NPM", + "Menu:Modules": "Módulos", + "Menu:Maintenance": "Mantenimiento", + "Menu:NugetPackages": "Paquetes Nuget", + "CreateAnOrganization": "Crear una organización", + "Organizations": "Organizaciones", + "LongName": "Nombre largo", + "LicenseType": "Tipo de licencia", + "MissingLicenseTypeField": "El campo tipo de licencia es requerido!", + "LicenseStartTime": "Fecha de inicio de licencia", + "LicenseEndTime": "Fecha de caducidad de licencia", + "AllowedDeveloperCount": "Número de desarrolladores permitidos", + "UserNameOrEmailAddress": "Nombre de usuario o", + "AddOwner": "Añadir propietario", + "UserName": "Nombre de usuario", + "Email": "Email", + "Developers": "Desarrolladores", + "AddDeveloper": "Añadir desarrollador", + "Create": "Crear", + "UserNotFound": "Usuario no encontrado", + "{0}WillBeRemovedFromDevelopers": "{0} será eliminado de desarrolladores, ¿deseas continuar?", + "{0}WillBeRemovedFromOwners": "{0} será eliminado de propietarios, ¿deseas continuar?", + "Computers": "Ordenadores", + "UniqueComputerId": "Id única de ordenador", + "LastSeenDate": "Fecha de visto por última vez", + "{0}Computer{1}WillBeRemovedFromRecords": "El ordenador {0} ({1}) será eliminado de los registros", + "OrganizationDeletionWarningMessage": "La organización será eliminada", + "DeletingLastOwnerWarningMessage": "Una organización debe tener al menos un propietario!. Por lo tanto, tu no puedes eliminar este propietario", + "This{0}AlreadyExistInThisOrganization": "Este/a {0} ya existe en esta organización", + "AreYouSureYouWantToDeleteAllComputers": "Estás seguro tu quieres eliminar todos los ordenadores", + "DeleteAll": "Eliminar todo", + "DoYouWantToCreateNewUser": "¿Quieres crear un nuevo usuario?", + "MasterModules": "Módulos maestros", + "OrganizationName": "Nombre de organización", + "CreationDate": "Fecha de creación", + "LicenseStartDate": "Fecha de inicio de licencia", + "LicenseEndDate": "Fecha de caducidad de licencia", + "OrganizationNamePlaceholder": "Nombre de organización...", + "TotalQuestionCountPlaceholder": "Número total de preguntas...", + "RemainingQuestionCountPlaceholder": "Número de preguntas pendientes", + "LicenseTypePlaceholder": "Tipo de licencia...", + "CreationDatePlaceholder": "Fecha de creación...", + "LicenseStartDatePlaceholder": "Fecha de inicio de licencia...", + "LicenseEndDatePlaceholder": "Fecha de caducidad de licencia...", + "UsernameOrEmail": "Usuario o email", + "UsernameOrEmailPlaceholder": "Usuario o email...", + "Member": "Miembro", + "PurchaseOrderNo": "Número de orden de compra", + "QuotationDate": "Fecha de presupuesto", + "CompanyName": "Nombre de empresa", + "CompanyAddress": "Dirección de empresa", + "Price": "Precio", + "DiscountText": "Texto de descuento", + "DiscountQuantity": "Cantidad de descuento", + "DiscountPrice": "Precio de descuento", + "Quotation": "Presupuesto", + "ExtraText": "Texto extra", + "ExtraAmount": "Cantidad extra", + "DownloadQuotation": "Descarga el presupuesto", + "Invoice": "Factura", + "TaxNumber": "Identificación fiscal", + "InvoiceNumber": "Número de factura", + "InvoiceDate": "Fecha de factura", + "InvoiceNote": "Nota de factura", + "Quantity": "Cantidad", + "AddProduct": "Añadir producto", + "AddProductWarning": "Tu necesitas añadir un producto!", + "TotalPrice": "Precio total", + "Generate": "Generar", + "MissingQuantityField": "El campo cantidad es requerido!", + "MissingPriceField": "El campo precio es requerido!", + "CodeUsageStatus": "Estado", + "Country": "País", + "DeveloperCount": "Cuentas de desarrollo", + "RequestCode": "Solicitud de código", + "WebSite": "Sitio web", + "GithubUsername": "Nombre de usuario Github", + "PhoneNumber": "Número de teléfono", + "ProjectDescription": "Descripción del proyecto", + "Referrer": "Referente", + "DiscountRequests": "Solicitud de descuento", + "Copylink": "Copiar Link", + "Disable": "Deshabilitar", + "Enable": "Habilitar", + "EnableSendEmail": "Habilitar el envío de Email", + "SendEmail": "Enviar Email", + "SuccessfullyDisabled": "Deshabilitado correctamente", + "SuccessfullyEnabled": "Habilitado correctamente", + "EmailSent": "Email enviado", + "SuccessfullySent": "Enviado correctamente", + "SuccessfullyDeleted": "Borrado correctamente", + "DiscountRequestDeletionWarningMessage": "La solicitud de descuento será borrada", + "BusinessType": "Tipo de negocio", + "TotalQuestionCount": "Número total de preguntas", + "RemainingQuestionCount": "Número de preguntas restantes", + "TotalQuestionMustBeGreaterWarningMessage": "TotalQuestionCount debe ser mayor que RemainingQuestionCount !", + "QuestionCountsMustBeGreaterThanZero": "TotalQuestionCount y RemainingQuestionCount debe ser cero o más grande que cero !", + "UnlimitedQuestionCount": "Número de preguntas ilimitadas", + "Notes": "Notas", + "Menu:Community": "Comunidad", + "Menu:Articles": "Artículos", + "Wait": "Esperar", + "Approve": "Aprobar", + "Reject": "Rechazar", + "Details": "Detalles", + "Url": "Url", + "Title": "Título", + "ContentSource": "Fuente de contenido", + "Status": "Estado", + "ReadArticle": "Leer artículo", + "ArticleHasBeenWaiting": "El artículo ha sido puesto en espera", + "ArticleHasBeenApproved": "El artículo ha sido aprobado", + "ArticleHasBeenRejected": "El artículo ha sido rechazado", + "Permission:Community": "Comunidad", + "Permission:CommunityArticle": "Artículo", + "Link": "Link", + "Enum:ContentSource:0": "Github", + "Enum:ContentSource:1": "Externo", + "Enum:Status:0": "En espera", + "Enum:Status:1": "Rechazado", + "Enum:Status:2": "Aprobado", + "Summary": "Resumen", + "AuthorName": "Nombre del autor", + "CoverImage": "Imagen de portada", + "RemoveCacheConfirmationMessage": "¿Estás seguro que quieres borrar la caché para \"{0}\" artículo?", + "SuccessfullyRemoved": "Borrada correctamente", + "RemoveCache": "" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/tr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/tr.json index b7f7cb41a5..1884b0025f 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/tr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/tr.json @@ -156,6 +156,13 @@ "RemainingQuestionCount": "Kalan soru sayısı", "TotalQuestionMustBeGreaterWarningMessage": "Toplam soru sayısı kalan soru sayısından büyük olmalıdır!", "QuestionCountsMustBeGreaterThanZero": "Toplam soru sayısı ve kalan soru sayısı sıfır veya sıfırdan daha büyük olmalıdır!", - "UnlimitedQuestionCount": "Sınırsız soru sayısı" + "UnlimitedQuestionCount": "Sınırsız soru sayısı", + "Language": "Dil", + "Optional": "Opsiyonel", + "CreateArticleLanguageInfo": "Makalenin yazıldığı dil", + "Enum:ContentSource:2": "Video İçerik", + "DeleteCoverImage": "Kapak Fotoğrafını Sil", + "DeleteCoverImageConfirmationMessage": "Kapak fotoğrafını \"{0}\" isimli makale için silmek istediğinize emin misiniz?", + "DeleteCoverImageSuccessMessage": "Kapak fotoğrafı başarılı bir şekilde silinmiştir" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/de-DE.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/de-DE.json new file mode 100644 index 0000000000..77e1b2090a --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/de-DE.json @@ -0,0 +1,33 @@ +{ + "culture": "de-DE", + "texts": { + "Volo.AbpIo.Domain:010004": "Maximale Mitgliederanzahl erreicht!", + "Volo.AbpIo.Domain:010005": "Miximale Besizeranzahl erreicht!", + "Volo.AbpIo.Domain:010006": "Dieser Benutzer ist bereits ein Besitzer in dieser Organisation!", + "Volo.AbpIo.Domain:010007": "Dieser Benutzer ist bereits ein Entwickler in dieser Organisation!", + "Volo.AbpIo.Domain:010008": "Die zulässige Entwickleranzahl darf nicht geringer sein als die aktuelle Entwickleranzahl!", + "Volo.AbpIo.Domain:010009": "Die zulässige Entwickleranzahl darf nicht kleiner als 0 sein!", + "Volo.AbpIo.Domain:010010": "Die maximale Anzahl der Mac-Adressen ist überschritten!", + "Volo.AbpIo.Domain:010011": "Die persönliche Lizenz kann nicht mehr als 1 Entwickler haben!", + "Volo.AbpIo.Domain:010012": "Die Lizenz kann nicht einen Monat nach Ablauf der Lizenz verlängert werden!", + "Volo.AbpIo.Domain:020001": "Dieses NPM-Paket konnte nicht gelöscht werden, da \"{NugetPackages}\" Nuget-Pakete von diesem Paket abhängig sind.", + "Volo.AbpIo.Domain:020002": "Dieses NPM-Paket konnte nicht gelöscht werden, da \"{Module}\" Module dieses Paket verwenden.", + "Volo.AbpIo.Domain:020003": "Dieses NPM-Paket konnte nicht gelöscht werden, da \"{Module}\" Module dieses Paket verwenden und \"{NugetPackages}\" Nuget-Pakete von diesem Paket abhängig sind.", + "Volo.AbpIo.Domain:020004": "Dieses Nuget-Paket konnte nicht gelöscht werden, da \"{Module}\" Module dieses Paket verwenden.", + "WantToLearn?": "Wollen Sie sich einlernen?", + "ReadyToGetStarted?": "Bereit anzufangen?", + "JoinOurCommunity": "Tritt unserer Community bei", + "GetStartedUpper": "LOSLEGEN", + "ForkMeOnGitHub": "Fork me on GitHub", + "Features": "Features", + "GetStarted": "Loslegen", + "Documents": "Unterlagen", + "Community": "Community", + "ContributionGuide": "Leitfaden für Mitwirkende", + "Blog": "Blog", + "Commercial": "Commercial", + "MyAccount": "Mein Benutzerkonto", + "SeeDocuments": "Siehe Unterlagen", + "Samples": "Beispiele" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en-GB.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en-GB.json new file mode 100644 index 0000000000..235a7c61d0 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en-GB.json @@ -0,0 +1,35 @@ +{ + "culture": "en-GB", + "texts": { + "Volo.AbpIo.Domain:010004": "Maximum member count reached!", + "Volo.AbpIo.Domain:010005": "Maximum owner count reached!", + "Volo.AbpIo.Domain:010006": "This user is already an owner in this organisation!", + "Volo.AbpIo.Domain:010007": "This user is already a developer in this organisation!", + "Volo.AbpIo.Domain:010008": "Allowed Developer Count can not be less then current developer count!", + "Volo.AbpIo.Domain:010009": "Allowed Developer Count can not be less then 0!", + "Volo.AbpIo.Domain:010010": "Maximum mac address count has been exceeded!", + "Volo.AbpIo.Domain:010011": "Personal license can't have more than 1 developer!", + "Volo.AbpIo.Domain:010012": "License can't be extended one month after license expires!", + "Volo.AbpIo.Domain:020001": "Couldn't delete this NPM Package because \"{NugetPackages}\" Nuget Packages are dependent to this package.", + "Volo.AbpIo.Domain:020002": "Couldn't delete this NPM Package because \"{Modules}\" Modules are using this package.", + "Volo.AbpIo.Domain:020003": "Couldn't delete this NPM Package because \"{Modules}\" Modules are using this package and \"{NugetPackages}\" Nuget Packages are dependent to this package.", + "Volo.AbpIo.Domain:020004": "Couldn't delete this Nuget Package because \"{Modules}\" Modules are using this package.", + "WantToLearn?": "Want to learn?", + "ReadyToGetStarted?": "Ready to get started?", + "JoinOurCommunity": "Join our community", + "GetStartedUpper": "GET STARTED", + "ForkMeOnGitHub": "Fork me on GitHub", + "Features": "Features", + "GetStarted": "Get Started", + "Documents": "Documents", + "Community": "Community", + "ContributionGuide": "Contribution Guide", + "Blog": "Blog", + "Commercial": "Commercial", + "MyAccount": "My account", + "Permission:License": "License", + "Permission:UserInfo": "User info", + "SeeDocuments": "See Documents", + "Samples": "Samples" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json index a916cd82d9..71dc425dc6 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json @@ -27,6 +27,8 @@ "Blog": "Blog", "Commercial": "Commercial", "MyAccount": "My account", + "Permission:License": "License", + "Permission:UserInfo": "Usere info", "SeeDocuments": "See Documents", "Samples": "Samples" } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/es.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/es.json new file mode 100644 index 0000000000..9974232511 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/es.json @@ -0,0 +1,33 @@ +{ + "culture": "es", + "texts": { + "Volo.AbpIo.Domain:010004": "Número máximo de miembros alcanzado!", + "Volo.AbpIo.Domain:010005": "Número máximo de propietarios alcanzado!", + "Volo.AbpIo.Domain:010006": "Este usuario ya es un propietario de esta organización!", + "Volo.AbpIo.Domain:010007": "Este usuario ya es un desarrollador en este organización!", + "Volo.AbpIo.Domain:010008": "Número de desarrolladores permitido no puede ser menor que el número actual de desarrolladores!", + "Volo.AbpIo.Domain:010009": "El número de desarrolladores no puede ser menor que cero!", + "Volo.AbpIo.Domain:010010": "Número máximo de dirección mac excedido!", + "Volo.AbpIo.Domain:010011": "Una licencia personal no puede tener más de un desarrollador!", + "Volo.AbpIo.Domain:010012": "La licencia no puede ser extendida un mes despues de que expire!", + "Volo.AbpIo.Domain:020001": "Este paquete NPM no pudo ser borrado porque \"{NugetPackages}\" paquetes Nuget son dependientes de este paquete.", + "Volo.AbpIo.Domain:020002": "Este paquete NPM no pudo ser borrado porque \"{Modules}\" modulos están usando este paquete.", + "Volo.AbpIo.Domain:020003": "Este paquete NPM no pudo ser borrado porque \"{Modules}\" modulos están usando este paquete.y \"{NugetPackages}\" paquetes Nuget son dependientes de este paquete.", + "Volo.AbpIo.Domain:020004": "Este paquete Nuget no pudo ser borrado porque \"{Modules}\" modulos están usando este paquete.", + "WantToLearn?": "¿Quieres aprender?", + "ReadyToGetStarted?": "¿Preparado para comenzar?", + "JoinOurCommunity": "Unete a nuestra comunidad", + "GetStartedUpper": "COMENZAR", + "ForkMeOnGitHub": "Fork en GitHub", + "Features": "Características", + "GetStarted": "Comenzar", + "Documents": "Documentos", + "Community": "Comunidad", + "ContributionGuide": "Guia de contribución", + "Blog": "Blog", + "Commercial": "Comercial", + "MyAccount": "Mi cuenta", + "SeeDocuments": "Ver documentos", + "Samples": "Ejemplos" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/zh-Hans.json index 79493b537e..f061408c98 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/zh-Hans.json @@ -27,6 +27,8 @@ "Blog": "博客", "Commercial": "商业版", "MyAccount": "我的账户", + "Permission:License": "许可", + "Permission:UserInfo": "用户信息", "SeeDocuments": "查看文档", "Samples": "示例" } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/de-DE.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/de-DE.json new file mode 100644 index 0000000000..ddc493e63b --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/de-DE.json @@ -0,0 +1,35 @@ +{ + "culture": "de-DE", + "texts": { + "OrganizationManagement": "Organisationsverwaltung", + "OrganizationList": "Organisationsauflistung", + "Volo.AbpIo.Commercial:010003": "Sie sind der Besitzer dieser Organisation!", + "OrganizationNotFoundMessage": "Keine Organisation gefunden!", + "DeveloperCount": "Zugeordnete / Gesamte Entwickler", + "QuestionCount": "Verbleibende / Gesamte Fragen", + "Unlimited": "Unbegrenzt", + "Owners": "Besitzer", + "AddMember": "Mitglied hinzufügen", + "AddOwner": "Besizer hinzufügen", + "AddDeveloper": "Entwickler hinzufügen", + "UserName": "Benutzername", + "Name": "Name", + "EmailAddress": "E-Mail-Adress", + "Developers": "Entwickler", + "LicenseType": "Lizenztyp", + "Manage": "Verwalten", + "StartDate": "Startdatum", + "EndDate": "Enddatum", + "Modules": "Module", + "LicenseExtendMessage": "Ihr Lizenzenddatum wird auf {0} verlängert", + "LicenseUpgradeMessage": "Ihre Lizenz wird auf {0} aktualisiert", + "LicenseAddDeveloperMessage": "{0} Entwickler zu Ihrer Lizenz hinzugefügt", + "Volo.AbpIo.Commercial:010004": "Kann den angegebenen Benutzer nicht finden! Der Benutzer muss sich bereits registriert haben.", + "MyOrganizations": "Meine Organisationen", + "ApiKey": "API-Schlüssel", + "UserNameNotFound": "Es gibt keinen Benutzer mit dem Benutzernamen {0}", + "SuccessfullyAddedToNewsletter": "Vielen Dank, dass Sie unseren Newsletter abonniert haben!", + "MyProfile": "Mein Profil", + "EmailNotValid": "Bitte geben Sie eine gültige E-Mail-Adresse ein." + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en-GB.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en-GB.json new file mode 100644 index 0000000000..56f4ed934f --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en-GB.json @@ -0,0 +1,39 @@ +{ + "culture": "en-GB", + "texts": { + "OrganizationManagement": "Organisation Management", + "OrganizationList": "Organisation list", + "Volo.AbpIo.Commercial:010003": "You are not owner of this organisation!", + "OrganizationNotFoundMessage": "Organisation not found!", + "DeveloperCount": "Allocated / total developers", + "QuestionCount": "Remaining / total questions", + "Unlimited": "Unlimited", + "Owners": "Owners", + "AddMember": "Add member", + "AddOwner": "Add owner", + "AddDeveloper": "Add developer", + "UserName": "Username", + "Name": "Name", + "EmailAddress": "Email address", + "Developers": "Developers", + "LicenseType": "License type", + "Manage": "Manage", + "StartDate": "Start date", + "EndDate": "End date", + "Modules": "Modules", + "LicenseExtendMessage": "Your license end date is extended to {0}", + "LicenseUpgradeMessage": "Your license is upgraded to {0}", + "LicenseAddDeveloperMessage": "{0} developers added to your license", + "Volo.AbpIo.Commercial:010004": "Cannot find the specified user! The user must have already registered.", + "MyOrganizations": "My organisations", + "ApiKey": "API key", + "UserNameNotFound": "There is no user with username {0}", + "SuccessfullyAddedToNewsletter": "Thank you for subscribing to our newsletter!", + "MyProfile": "My Profile", + "EmailNotValid": "Please enter a valid email address.", + "JoinOurMarketingNewsletter": "Join our marketing newsletter", + "WouldLikeToReceiveMarketingMaterials": "I would like to receive marketing materials e.g. product deals & special offers.", + "StartUsingYourLicenseNow": "Start using your license now!", + "WelcomePage": "Welcome Page" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index 2a57c6b25d..c6eb1ad645 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -30,6 +30,10 @@ "UserNameNotFound": "There is no user with username {0}", "SuccessfullyAddedToNewsletter": "Thanks you for subscribing to our newsletter!", "MyProfile": "My profile", - "EmailNotValid": "Please enter a valid email address." + "EmailNotValid": "Please enter a valid email address.", + "JoinOurMarketingNewsletter": "Join our marketing newsletter", + "WouldLikeToReceiveMarketingMaterials": "I would like to receive marketing materials like product deals & special offers.", + "StartUsingYourLicenseNow": "Start using your license now!", + "WelcomePage": "Welcome Page" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/es.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/es.json new file mode 100644 index 0000000000..80647e85e9 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/es.json @@ -0,0 +1,35 @@ +{ + "culture": "es", + "texts": { + "OrganizationManagement": "Gestión de la organización", + "OrganizationList": "Lista organización", + "Volo.AbpIo.Commercial:010003": "Tu no eres el propietario de esta organización!", + "OrganizationNotFoundMessage": "Organización no encontrada!", + "DeveloperCount": "Total desarrolladores asignados", + "QuestionCount": "Total de preguntas restantes", + "Unlimited": "Ilimitado", + "Owners": "Propietarios", + "AddMember": "Añadir miembro", + "AddOwner": "Añadir propietario", + "AddDeveloper": "Añadir desarrollador", + "UserName": "Nombre de usuario", + "Name": "Nombre", + "EmailAddress": "Dirección de Email", + "Developers": "Desarrolladores", + "LicenseType": "Tipo de licencia", + "Manage": "Gestionar", + "StartDate": "Fecha de inicio", + "EndDate": "Fecha de fin", + "Modules": "Módulos", + "LicenseExtendMessage": "Tu fecha de finalización de tu licencia ha sido extendido a {0}", + "LicenseUpgradeMessage": "Tu licencia esta actualizada a {0}", + "LicenseAddDeveloperMessage": "{0} desarrolladores añadidos a tu licencia", + "Volo.AbpIo.Commercial:010004": "No se pudo encontrar el usuario especificado. El usuario debe estar ya registrado.", + "MyOrganizations": "Mis organizaciones", + "ApiKey": "API Key", + "UserNameNotFound": "No hay un usuario con el nombre de usuario {0}", + "SuccessfullyAddedToNewsletter": "Gracias por suscribirte a nuestro boletín de noticias!", + "MyProfile": "Mi perfil", + "EmailNotValid": "Por favor, introduce una dirección de email válida." + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json index 0147e71a05..82c23450a0 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json @@ -28,8 +28,12 @@ "MyOrganizations": "我的组织", "ApiKey": "API key", "UserNameNotFound": "没有用户名为{0}的用户", - "SuccessfullyAddedToNewsletter": "感谢你订阅我们的新闻通讯!", + "SuccessfullyAddedToNewsletter": "感谢你订阅我们的新闻简讯!", "MyProfile": "我的资料", - "EmailNotValid": "请输入有效的电子邮件地址" + "EmailNotValid": "请输入有效的电子邮件地址", + "JoinOurMarketingNewsletter": "加入我们的营销简讯", + "WouldLikeToReceiveMarketingMaterials": "我想收到市场营销资料,例如产品交易和特别优惠.", + "StartUsingYourLicenseNow": "立即开始使用你的许可证", + "WelcomePage": "欢迎页面" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de-DE.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de-DE.json new file mode 100644 index 0000000000..0db3c9b29b --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de-DE.json @@ -0,0 +1,90 @@ +{ + "culture": "de-DE", + "texts": { + "Permission:CommunityArticle": "Community-Beitrag", + "Permission:Edit": "Bearbeiten", + "Waiting": "Wartend", + "Approved": "Genehmigt", + "Rejected": "Abgelehnt", + "Wait": "Warten", + "Approve": "Genehmigen", + "Reject": "Ablehnen", + "ReadArticle": "Beitrag lesen", + "Status": "Status", + "ContentSource": "Inhaltsquelle", + "Details": "Details", + "Url": "URL", + "Title": "Titel", + "CreationTime": "Erstellungszeitpunkt", + "Save": "Speichern", + "SameUrlAlreadyExist": "Dieselbe URL existiert bereits, wenn Sie diesen Beitrag hinzufügen möchten, sollten Sie die URL ändern!", + "UrlIsNotValid": "Der URL ist nicht korrekt.", + "UrlNotFound": "URL nicht gefunden.", + "UrlContentNotFound": "URL-Inhalt nicht gefunden", + "Summary": "Zusammenfassung", + "MostRead": "Meist gelesen", + "Latest": "Neueste", + "ContributeAbpCommunity": "Tragen Sie zur ABP Community bei", + "SubmitYourArticle": "Reichen Sie Ihren Beitrag ein", + "ContributionGuide": "Leitfaden für Mitwirkende", + "BugReport": "Fehler melden", + "SeeAllArticles": "Alle Beiträge anzeigen", + "WelcomeToABPCommunity!": "Willkommen in der ABP Community!", + "MyProfile": "Mein Profil", + "MyOrganizations": "Meine Organisationen", + "EmailNotValid": "Bitte geben Sie eine gültige E-Mail-Adresse ein.", + "FeatureRequest": "Featureanfrage", + "CreateArticleTitleInfo": "Titel des Beitrags, der in der Beitragsliste angezeigt werden soll.", + "CreateArticleUrlInfo": "Original GitHub-/externe URL des Beitrags.", + "CreateArticleSummaryInfo": "Eine kurze Zusammenfassung des Beitrags, der in der Beitragsliste angezeigt werden soll.", + "CreateArticleCoverInfo": "Fügen Sie zum Erstellen eines effektiven Beitrags ein Titelbild hinzu. Laden Sie Bilder mit einem Seitenverhältnis von 16: 9 hoch, um die beste Ansicht zu erhalten.", + "ThisExtensionIsNotAllowed": "Diese Erweiterung ist nicht zulässig.", + "TheFileIsTooLarge": "Die Datei ist zu groß.", + "GoToTheArticle": "Gehe zum Beitrag", + "Contribute": "Beitragen", + "OverallProgress": "Gesamtfortschritt", + "Done": "Fertig", + "Open": "Offen", + "Closed": "Geschlossen", + "LatestQuestionOnThe": "Letzte Frage zum", + "Stackoverflow": "Stackoverflow", + "Votes": "Stimmen", + "Answer": "Antwort", + "Views": "Ansichten", + "Answered": "Beantwortet", + "WaitingForYourAnswer": "Warten auf Ihre Antwort", + "Asked": "gefragt", + "AllQuestions": "Alle Fragen", + "NextVersion": "Nächste Version", + "MilestoneErrorMessage": "Die aktuellen Meilensteindetails konnten von Github nicht abgerufen werden.", + "QuestionItemErrorMessage": "Die neuesten Fragendetails konnten von Stackoverflow nicht abgerufen werden.", + "Oops": "Hoppla!", + "CreateArticleSuccessMessage": "Der Beitrag wurde erfolgreich eingereicht. Er wird nach einer Überprüfung durch den Site-Administrator veröffentlicht.", + "ChooseCoverImage": "Ein Titelbild auswählen...", + "CoverImage": "Titelbild", + "ShareYourExperiencesWithTheABPFramework": "Ihre Erfahrungen mit dem ABP Framework teilen!", + "Optional": "Optional", + "UpdateUserWebSiteInfo": "Beispiel: https://johndoe.com", + "UpdateUserTwitterInfo": "Beispiel: johndoe", + "UpdateUserGithubInfo": "Beispiel: johndoe", + "UpdateUserLinkedinInfo": "Beispiel: https://www.linkedin.com/...", + "UpdateUserCompanyInfo": "Beispiel: Volosoft", + "UpdateUserJobTitleInfo": "Beispiel: Software Developer", + "UserName": "Benutzername", + "Company": "Firma", + "PersonalWebsite": "Persönliche Website", + "RegistrationDate": "Registrierungsdatum", + "Social": "Social", + "Biography": "Biographie", + "HasNoPublishedArticlesYet": "hat noch keine Beiträge veröffentlicht", + "Author": "Autor", + "LatestGithubAnnouncements": "Neueste Github-Ankündigungen", + "SeeAllAnnouncements": "Alle Ankündigungen anzeigen", + "LatestBlogPost": "Letzter Blog-Beitrag", + "Edit": "Bearbeiten", + "ProfileImageChange": "Ändern Sie das Profilbild", + "BlogItemErrorMessage": "Die neuesten Blogpost-Details konnten nicht abgerufen werden.", + "PlannedReleaseDate": "Geplantes Erscheinungsdatum", + "CommunityArticleRequestErrorMessage": "Die Anfrage nach den neuesten Beiträgen von Github konnte nicht abgerufen werden." + } +} diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en-GB.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en-GB.json new file mode 100644 index 0000000000..652090d864 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en-GB.json @@ -0,0 +1,106 @@ +{ + "culture": "en-GB", + "texts": { + "Permission:CommunityArticle": "Community Article", + "Permission:Edit": "Edit", + "Waiting": "Waiting", + "Approved": "Approved", + "Rejected": "Rejected", + "Wait": "Wait", + "Approve": "Approve", + "Reject": "Reject", + "ReadArticle": "Read Article", + "Status": "Status", + "ContentSource": "Content Source", + "Details": "Details", + "Url": "Url", + "Title": "Title", + "CreationTime": "Creation time", + "Save": "Save", + "SameUrlAlreadyExist": "The Url already exists. If you want to add this article, you should change the url!", + "UrlIsNotValid": "Url is not valid.", + "UrlNotFound": "Url not found.", + "UrlContentNotFound": "Url content not found.", + "Summary": "Summary", + "MostRead": "Most Read", + "Latest": "Latest", + "ContributeAbpCommunity": "Contribute to the ABP Community", + "SubmitYourArticle": "Submit Your Article", + "ContributionGuide": "Contribution Guide", + "BugReport": "Bug Report", + "SeeAllArticles": "See All Articles", + "WelcomeToABPCommunity!": "Welcome to the ABP Community!", + "MyProfile": "My Profile", + "MyOrganizations": "My Organisations", + "EmailNotValid": "Please enter a valid email address.", + "FeatureRequest": "Feature Request", + "CreateArticleTitleInfo": "Title of the article to be shown on the article list.", + "CreateArticleUrlInfo": "Original GitHub/External URL of the article.", + "CreateArticleSummaryInfo": "A short summary of the article to be shown on the article list.", + "CreateArticleCoverInfo": "To create an effective article, add a cover photo and an upload 16:9 aspect ratio pictures for the best view (Maximum file size: 1MB)", + "ThisExtensionIsNotAllowed": "The extension is not allowed.", + "TheFileIsTooLarge": "The file is too large.", + "GoToTheArticle": "Go to the Article", + "Contribute": "Contribute", + "OverallProgress": "Overall Progress", + "Done": "Done", + "Open": "Open", + "Closed": "Closed", + "LatestQuestionOnThe": "Latest Question on the", + "Stackoverflow": "Stackoverflow", + "Votes": "votes", + "Answer": "Answer", + "Views": "views", + "Answered": "Answered", + "WaitingForYourAnswer": "Waiting for your answer", + "Asked": "asked", + "AllQuestions": "All Questions", + "NextVersion": "Next Version", + "MilestoneErrorMessage": "Couldn't get the current milestone details from Github.", + "QuestionItemErrorMessage": "Couldn't get the latest question details from Stackoverflow.", + "Oops": "Oops!", + "CreateArticleSuccessMessage": "The Article has been successfully submitted. It will be published once it has been reviewed by the site admin.", + "ChooseCoverImage": "Choose a cover image...", + "CoverImage": "Cover Image", + "ShareYourExperiencesWithTheABPFramework": "Share your experiences with the ABP Framework!", + "Optional": "Optional", + "UpdateUserWebSiteInfo": "Example: https://johndoe.com", + "UpdateUserTwitterInfo": "Example: johndoe", + "UpdateUserGithubInfo": "Example: johndoe", + "UpdateUserLinkedinInfo": "Example: https://www.linkedin.com/...", + "UpdateUserCompanyInfo": "Example: Volosoft", + "UpdateUserJobTitleInfo": "Example: Software Developer", + "UserName": "UserName", + "Company": "Company", + "PersonalWebsite": "Personal website", + "RegistrationDate": "Registration date", + "Social": "Social", + "Biography": "Biography", + "HasNoPublishedArticlesYet": "has no published articles yet", + "Author": "Author", + "LatestGithubAnnouncements": "Latest GitHub Announcements", + "SeeAllAnnouncements": "View all Announcements", + "LatestBlogPost": "Latest Blog Post", + "Edit": "Edit", + "ProfileImageChange": "Change the profile image", + "BlogItemErrorMessage": "Couldn't get the latest blog post details from ABP.", + "PlannedReleaseDate": "Planned release date", + "CommunityArticleRequestErrorMessage": "Couldn't get the latest article request from GitHub.", + "ArticleRequestFromGithubIssue": "There aren't any article requests at the moment.", + "LatestArticles": "Latest Articles", + "ArticleRequests": "Article Requests", + "AllArticleRequests": "See all Article requests", + "SubscribeToTheNewsletter": "Subscribe to the Newsletter", + "NewsletterEmailDefinition": "Receive information about what's happening in ABP; new releases, free sources, articles, and more.", + "NoThanks": "No thanks", + "MaybeLater": "Maybe later", + "JoinOurArticleNewsletter": "Join our article newsletter", + "Community": "Community", + "Marketing": "Marketing", + "CommunityPrivacyPolicyConfirmation": "I agree to the Terms & Conditions and Privacy Policy.", + "ArticleRequestMessageTitle": "Open an issue on the GitHub to request an article/tutorial you want to see on this web site.", + "ArticleRequestMessageBody": "Here is the list of the requested articles by the Community. Do you want to write a requested article? Please click on the request and join the discussion.", + "Language": "Language", + "CreateArticleLanguageInfo": "The language in which the article is written" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json index 87db8c931a..5bf922f012 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json @@ -25,19 +25,18 @@ "MostRead": "Most Read", "Latest": "Latest", "ContributeAbpCommunity": "Contribute to the ABP Community", - "SubmitYourArticle": "Submit Your Article", + "SubmitYourArticle": "Submit Your Post", "ContributionGuide": "Contribution Guide", "BugReport": "Bug Report", - "SeeAllArticles": "See All Articles", + "SeeAllArticles": "See All Posts", "WelcomeToABPCommunity!": "Welcome to the ABP Community!", "MyProfile": "My profile", "MyOrganizations": "My organizations", "EmailNotValid": "Please enter a valid email address.", "FeatureRequest": "Feature Request", - "CreateArticleTitleInfo": "Title of the article to be shown on the article list.", - "CreateArticleUrlInfo": "Original GitHub/External URL of the article.", - "CreateArticleSummaryInfo": "A short summary of the article to be shown on the article list.", - "CreateArticleCoverInfo": "For creating an effective article, add a cover photo. Upload 16:9 aspect ratio pictures for the best view.", + "CreateArticleTitleInfo": "Title of the post to be shown on the post list.", + "CreateArticleSummaryInfo": "A short summary of the post to be shown on the post list.", + "CreateArticleCoverInfo": "For creating an effective article, add a cover photo. Upload 16:9 aspect ratio pictures for the best view. Maximum file size: 1MB.", "ThisExtensionIsNotAllowed": "This extension is not allowed.", "TheFileIsTooLarge": "The file is too large.", "GoToTheArticle": "Go to the Article", @@ -85,6 +84,54 @@ "ProfileImageChange": "Change the profile image", "BlogItemErrorMessage": "Could not get the latest blog post details from ABP.", "PlannedReleaseDate": "Planned release date", - "CommunityArticleRequestErrorMessage": "Could not get the latest article request from Github." + "CommunityArticleRequestErrorMessage": "Could not get the latest article request from Github.", + "ArticleRequestFromGithubIssue": "There are not any article requests now.", + "LatestArticles": "Latest Posts", + "ArticleRequests": "Article Requests", + "AllArticleRequests": "See All Article Requests", + "SubscribeToTheNewsletter": "Subscribe to the Newsletter", + "NewsletterEmailDefinition": "Get information about happenings in ABP like new releases, free sources, articles, and more.", + "NoThanks": "No, thanks", + "MaybeLater": "Maybe later", + "JoinOurArticleNewsletter": "Join our article newsletter", + "Community": "Community", + "Marketing": "Marketing", + "CommunityPrivacyPolicyConfirmation": "I agree to the Terms & Conditions and Privacy Policy.", + "ArticleRequestMessageTitle": "Open an issue on the GitHub to request an article/tutorial you want to see on this web site.", + "ArticleRequestMessageBody": "Here, the list of the requested articles by the community. Do you want to write a requested article? Please click to the request and join to the discussion.", + "Language": "Language", + "CreateArticleLanguageInfo": "The language for the post content.", + "VideoPost": "Video Post", + "Article": "Article", + "Read": "Read", + "CreateGithubArticleUrlInfo": "Original GitHub URL of the article.", + "CreateVideoContentUrlInfo": "Original Youtube URL of the post.", + "CreateExternalArticleUrlInfo": "Original External Url of the article.", + "VideoContentForm": "Submit Video on YouTube", + "GithubPostForm": "Submit Article on GitHub", + "ExternalPostForm": "Submit an External Content", + "HowToPost": "How to Post?", + "Posts": "Posts", + "VideoUrl": "Video Url", + "GithubArticleUrl": "Github Article Url", + "ExternalArticleUrl": "External Article Url", + "CreatePostCoverInfo": "For creating an effective post, add a cover photo. Upload 16:9 aspect ratio pictures for the best view. Maximum file size: 1MB.", + "ThankYouForContribution": "Thank you for contributing to the ABP Community.", + "GithubArticle": "Github Article", + "GithubArticleSubmitStepOne": "1. Write an article on any public GitHub repository with the Markdown format. example", + "GithubArticleSubmitStepTwo": "2. Submit your article URL using the form.", + "GithubArticleSubmitStepThree": "3. Your article will be rendered in this web site.", + "YoutubeVideo": "Youtube Video", + "YoutubeVideoSubmitStepOne": "1. Publish your video on YouTube.", + "YoutubeVideoSubmitStepTwo": "2. Submit the video URL using the form.", + "YoutubeVideoSubmitStepThree": "3. Visitors will be able to watch your video content directly on this website.", + "ExternalContent": "External Content", + "ExternalContentSubmitStepOne": "1. Create a content on any public platform (medium, your own blog or anywhere you like).", + "ExternalContentSubmitStepTwo": "2. Submit your content URL using the form.", + "ExternalContentSubmitStepThree": "3. Visitors are redirected to the content on the original website.", + "ChooseYourContentType": "Please choose the way you want to add your content.", + "PostContentViaGithub": "I want to add my article with GitHub in accordance with the markdown rules.", + "PostContentViaYoutube": "I want to share my videos available on Youtube here.", + "PostContentViaExternalSource": "I want to add the content I published on another platform here." } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json new file mode 100644 index 0000000000..433de31852 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json @@ -0,0 +1,90 @@ +{ + "culture": "es", + "texts": { + "Permission:CommunityArticle": "Artículo de comunidad", + "Permission:Edit": "Editar", + "Waiting": "Esperando", + "Approved": "Aprobado", + "Rejected": "Rechazado", + "Wait": "Esperar", + "Approve": "Aprobar", + "Reject": "Rechazar", + "ReadArticle": "Leer artículo", + "Status": "Estado", + "ContentSource": "Fuente de contenido", + "Details": "Detalles", + "Url": "Url", + "Title": "Título", + "CreationTime": "Fecha de creación", + "Save": "Guardar", + "SameUrlAlreadyExist": "La url ya existe si tu quieres añadir este artículo, tu debes cambiar la url!", + "UrlIsNotValid": "Url no es valida", + "UrlNotFound": "Url no encontrada", + "UrlContentNotFound": "Contenido de la Url no encontrado", + "Summary": "Resumen", + "MostRead": "Más leído", + "Latest": "Últimos", + "ContributeAbpCommunity": "Contribuye a la comunidad ABP", + "SubmitYourArticle": "Envía tu artículo", + "ContributionGuide": "Guía de contribución", + "BugReport": "Informe de errores", + "SeeAllArticles": "Ver todos los artículos", + "WelcomeToABPCommunity!": "Bienvenido a la comunidad ABP", + "MyProfile": "Mi perfil", + "MyOrganizations": "Mis organizaciones", + "EmailNotValid": "Por favor entra una dirección de email válida.", + "FeatureRequest": "Solucitud de característica", + "CreateArticleTitleInfo": "Título del artículo para ser mostrado en la lista de artículos.", + "CreateArticleUrlInfo": "Url original del artículo GitHub/ Externo", + "CreateArticleSummaryInfo": "Un pequeño resumen del artículo para ser mostrado en la lista de artículos.", + "CreateArticleCoverInfo": "Para crear un artículo eficaz, agregue una foto de portada. Cargue imágenes con una relación de aspecto de 16: 9 para obtener la mejor vista.", + "ThisExtensionIsNotAllowed": "Esta extensión no está permitida.", + "TheFileIsTooLarge": "El fichero es demasiado grande.", + "GoToTheArticle": "Ir a el artículo", + "Contribute": "Contribuir", + "OverallProgress": "Progreso general", + "Done": "Hecho", + "Open": "Abrir", + "Closed": "Cerrado", + "LatestQuestionOnThe": "Última pregunta en la", + "Stackoverflow": "Stackoverflow", + "Votes": "Votos", + "Answer": "Respuesta", + "Views": "Vistas", + "Answered": "Respondido", + "WaitingForYourAnswer": "Esperando tu respuesta", + "Asked": "Preguntado", + "AllQuestions": "Todas las preguntas", + "NextVersion": "Siguiente versión", + "MilestoneErrorMessage": "No se pudieron obtener los detalles del hito actual en Github.", + "QuestionItemErrorMessage": "no se pudieron obtener los detalles de pregunta actual en Stackoverflow.", + "Oops": "Oops!", + "CreateArticleSuccessMessage": "El artículo se ha enviado correctamente. Se publicará después de una revisión del administrador del sitio.", + "ChooseCoverImage": "Elige una imagen de portada...", + "CoverImage": "Imagen de portada", + "ShareYourExperiencesWithTheABPFramework": "Comparte tus experiencias con el ABP Framework!", + "Optional": "Opcional", + "UpdateUserWebSiteInfo": "Ejemplo: https://johndoe.com", + "UpdateUserTwitterInfo": "Ejemplo: johndoe", + "UpdateUserGithubInfo": "Ejemplo: johndoe", + "UpdateUserLinkedinInfo": "Ejemplo: https://www.linkedin.com/...", + "UpdateUserCompanyInfo": "Ejemplo: Volosoft", + "UpdateUserJobTitleInfo": "Ejemplo: desarrollador de software", + "UserName": "Nombre de usuario", + "Company": "Empresa", + "PersonalWebsite": "Sitio web personal", + "RegistrationDate": "Fecha de registro", + "Social": "Social", + "Biography": "Biografía", + "HasNoPublishedArticlesYet": "No has publicado articules todavía", + "Author": "Autor", + "LatestGithubAnnouncements": "Últimas notificaciones de Github", + "SeeAllAnnouncements": "Ver todos las notificaciones", + "LatestBlogPost": "", + "Edit": "Editar", + "ProfileImageChange": "Cambiar la imagen de perfil", + "BlogItemErrorMessage": "No se pudo obtener los detalles del último blog desde ABP.", + "PlannedReleaseDate": "Fecha de entrega planificada", + "CommunityArticleRequestErrorMessage": "No se pudo obtener la última petición de artículo desde Github" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json index fa793c91a0..49b819227f 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json @@ -35,7 +35,6 @@ "EmailNotValid": "请输入有效的电子邮箱地址.", "FeatureRequest": "功能请求", "CreateArticleTitleInfo": "文章标题显示在文章列表中.", - "CreateArticleUrlInfo": "文章的原始GitHub/外部URL.", "CreateArticleSummaryInfo": "文章的简短摘要将显示在文章列表中.", "CreateArticleCoverInfo": "为了创建有效的文章,请添加封面图. 仅支持16:9的图片!", "ThisExtensionIsNotAllowed": "不允许此扩展名.", @@ -84,6 +83,34 @@ "Edit": "修改", "ProfileImageChange": "更改资料图片", "BlogItemErrorMessage": "无法从ABP获取最新的博客文章详细信息.", - "PlannedReleaseDate": "计划发布日期" + "PlannedReleaseDate": "计划发布日期", + "CommunityArticleRequestErrorMessage": "无法从Github获取最新的文章请求.", + "ArticleRequestFromGithubIssue": "现在没有任何文章请求.", + "LatestArticles": "最新的帖子", + "ArticleRequests": "文章请求", + "AllArticleRequests": "查看所有文章请求", + "SubscribeToTheNewsletter": "订阅简讯", + "NewsletterEmailDefinition": "获取有关ABP发生的信息,例如新版本,免费资源,文章等.", + "NoThanks": "不用了,谢谢", + "MaybeLater": "以后再说", + "JoinOurArticleNewsletter": "加入我们的文章简讯", + "Community": "社区", + "Marketing": "营销", + "CommunityPrivacyPolicyConfirmation": "我同意条款和条件以及隐私政策.", + "ArticleRequestMessageTitle": "在GitHub上创建一个Issue,以请求你要在此网站上查看的文章/教程.", + "ArticleRequestMessageBody": "在这里,是社区请求的文章列表. 您要写一篇要求的文章吗? 请单击该请求并加入讨论.", + "Language": "语言", + "CreateArticleLanguageInfo": "本文所用的语言", + "VideoPost": "视频", + "Article": "文章", + "Read": "阅读", + "CreateGithubArticleUrlInfo": "文章的原始GitHub链接.", + "CreateVideoContentUrlInfo": "文章的原始Youtube链接.", + "CreateExternalArticleUrlInfo": "本文的原始外部网址", + "VideoContentForm": "视频内容来源", + "GithubPostForm": "Github文章来源", + "ExternalPostForm": "外部文章来源", + "PostSourceTypeChooses": "我们接受文章的三种来源类型;", + "Posts": "文章" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ar.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ar.json index a5d83f9807..3160dd45b4 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ar.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ar.json @@ -156,6 +156,7 @@ "UiFramework": "إطار عمل واجهة المستخدم", "EmailAddress": "البريد الإلكترونى", "Mobile": "المحمول", - "ReactNative": "React Native" + "ReactNative": "React Native", + "SelectLanguage": "اختار اللغة" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/de-DE.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/de-DE.json new file mode 100644 index 0000000000..523247a7b6 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/de-DE.json @@ -0,0 +1,190 @@ +{ + "culture": "de-DE", + "texts": { + "GetStarted": "Erste Schritte - Startvorlagen", + "Create": "Erstellen", + "NewProject": "Neues Projekt", + "DirectDownload": "Direkter Download", + "ProjectName": "Proejktname", + "ProjectType": "Projekttyp", + "DatabaseProvider": "Datenbankanbieter", + "NTier": "N-Tier", + "IncludeUserInterface": "Benutzeroberfläche einschließen", + "CreateNow": "Jetzt erstellen", + "TheStartupProject": "Das Startprojekt", + "Tutorial": "Lernprogramm", + "UsingCLI": "mit CLI", + "SeeDetails": "Details ansehen", + "AbpShortDescription": "ABP Framework ist eine vollständige Infrastruktur zum Erstellen moderner Webanwendungen unter Befolgung von Best Practices und Konventionen für Softwareentwicklung.", + "SourceCodeUpper": "QUELLCODE", + "LatestReleaseLogs": "Akteulle Release", + "Infrastructure": "Infrastrutkur", + "Architecture": "Architektur", + "Modular": "Modular", + "DontRepeatYourself": "Don't Repeat Yourself", + "DeveloperFocused": "Entwickler-Zentriert", + "FullStackApplicationInfrastructure": "Full-Stack-Anwendungsinfrastruktur.", + "DomainDrivenDesign": "Domain Driven Design", + "DomainDrivenDesignExplanation": "Entworfen und entwickelt basierend auf DDD-Mustern und -Prinzipien. Bietet ein Schichtenmodell für Ihre Anwendung.", + "Authorization": "Authorization", + "AuthorizationExplanation": "Erweiterte Autorisierung mit Benutzer-, Rollen- und fein abgestimmtem Berechtigungssystem. Aufbauend auf der Microsoft Identity-Bibliothek.", + "MultiTenancy": "Multi-Tenancy", + "MultiTenancyExplanationShort": "SaaS-Anwendungen leicht gemacht! Integrierte Mandantenfähigkeit von der Datenbank bis zur Benutzeroberfläche.", + "CrossCuttingConcerns": "Cross Cutting Concerns", + "CrossCuttingConcernsExplanationShort": "Komplette Infrastruktur für Autorisierung, Validierung, Ausnahmebehandlung, Caching, Überwachungsprotokollierung, Transaktionsverwaltung und mehr.", + "BuiltInBundlingMinification": "Built-In Bundling & Minification", + "BuiltInBundlingMinificationExplanation": "Für die Bundling und Minification müssen keine externen Tools verwendet werden. ABP bietet eine einfachere, dynamische, leistungsstarke, modulare und integrierte Methode!", + "VirtualFileSystem": "Virtual File System", + "VirtualFileSystemExplanation": "Betten Sie Ansichten, Skripte, Stile, Bilder ... in Pakete/Bibliotheken ein und verwenden Sie sie in verschiedenen Anwendungen wieder.", + "Theming": "Theming", + "ThemingExplanationShort": "Verwenden und passen Sie das Bootstrap-basierte Standard-UI-Design an oder erstellen Sie Ihr eigenes.", + "BootstrapTagHelpersDynamicForms": "Bootstrap Tag Helpers & Dynamic Forms", + "BootstrapTagHelpersDynamicFormsExplanation": "Anstatt die sich wiederholenden Details von Bootstrap-Komponenten manuell zu schreiben, verwenden Sie die Tag-Helper von ABP, um diese zu vereinfachen und dabei die Vorteile von Intellisense zu nutzen. Erstellen Sie mit dem dynamischen Formular-Tag-Helfer schnell UI-Formulare basierend auf einem C#-Modell.", + "HTTPAPIsDynamicProxies": "HTTP APIs & Dynamic Proxies", + "HTTPAPIsDynamicProxiesExplanation": "Stellen Sie Anwendungsdienste automatisch als HTTP-APIs im REST-Stil bereit und verwenden Sie diese mit dynamischen JavaScript- und C#-Proxys.", + "CompleteArchitectureInfo": "Moderne Architektur zur Erstellung wartbarer Softwarelösungen.", + "DomainDrivenDesignBasedLayeringModelExplanation": "Hilft Ihnen bei der Implementierung einer DDD-basierten Schichtarchitektur und beim Aufbau einer wartbaren Codebasis.", + "DomainDrivenDesignBasedLayeringModelExplanationCont": "Bietet Startvorlagen, Abstraktionen, Basisklassen, Dienste, Dokumentation und Anleitungen, mit denen Sie Ihre Anwendung basierend auf DDD-Mustern und -Prinzipien entwickeln können.", + "MicroserviceCompatibleModelExplanation": "Das Kernframework und die vorgefertigten Module sind unter Berücksichtigung der Microservice-Architektur konzipiert.", + "MicroserviceCompatibleModelExplanationCont": "Bietet Infrastruktur, Integrationen, Beispiele und Dokumentation zur einfacheren Implementierung von Microservice-Lösungen, ohne zusätzliche Komplexität zu verursachen, wenn Sie eine monolithische Anwendung wünschen.", + "ModularInfo": "ABP bietet ein Modulsystem, mit dem Sie wiederverwendbare Anwendungsmodule entwickeln, Ereignisse im Anwendungslebenszyklus verknüpfen und Abhängigkeiten zwischen Kernteilen Ihres Systems ausdrücken können.", + "PreBuiltModulesThemes": "Vorgefertigte Module & Themes", + "PreBuiltModulesThemesExplanation": "Open Source- und kommerzielle Module und Themes stehen bereit, um in Ihrer Geschäftsanwendung verwendet zu werden.", + "NuGetNPMPackages": "NuGet- & NPM-Pakete", + "NuGetNPMPackagesExplanation": "Bereitgestellt als NuGet- & NPM-Pakete. Einfach zu installieren und zu aktualisieren.", + "ExtensibleReplaceable": "Erweiterbar/Austauschbar", + "ExtensibleReplaceableExplanation": "Alle Dienste und Module sind auf Erweiterbarkeit ausgelegt. Sie können Dienste, Seiten, Stile und Komponenten ersetzen.", + "CrossCuttingConcernsExplanation2": "Halten Sie Ihre Codebasis kleiner, damit Sie sich auf ihre geschäftsspezifischen Code konzentrieren können.", + "CrossCuttingConcernsExplanation3": "Verbringen Sie keine Zeit damit, grundlegende Anwendungsanforderungen für jedes neue Projekte zu implementieren.", + "AuthenticationAuthorization": "Authentifizierung & Autorisierung", + "ExceptionHandling": "Fehlerbehandlung", + "Validation": "Validierung", + "DatabaseConnection": "Datenbankverbindung", + "TransactionManagement": "Transaktionsmanagement", + "AuditLogging": "Audit Logging", + "Caching": "Caching", + "Multitenancy": "Multimandantenfähigkeit", + "DataFiltering": "Datenfilterung", + "ConventionOverConfiguration": "Convention Over Configuration", + "ConventionOverConfigurationExplanation": "ABP implementiert standardmäßig allgemeine Anwendungskonventionen mit einer minimalen oder Null-Konfiguration.", + "ConventionOverConfigurationExplanationList1": "Automatische Registrierung bekannter Services für Dependency Injection.", + "ConventionOverConfigurationExplanationList2": "Stellt Anwendungsdienste mittels Namenskonventionen als HTTP-APIs bereit.", + "ConventionOverConfigurationExplanationList3": "Erstellt dynamische HTTP-Client-Proxys für C# und JavaScript.", + "ConventionOverConfigurationExplanationList4": "Bietet Standard-Repositorys für Ihre Entities.", + "ConventionOverConfigurationExplanationList5": "Verwaltet die Unit-of-Work gemäß Webanforderung oder Anwendungsdienstmethode.", + "ConventionOverConfigurationExplanationList6": "Triggert Erstellungs-, Aktualisierungs- und Lösch-Events für Ihre Entities.", + "BaseClasses": "Basisklassen", + "BaseClassesExplanation": "Vorgefertigte Basisklassen für gängige Anwendungsmuster.", + "DeveloperFocusedExplanation": "ABP ist für Entwickler", + "DeveloperFocusedExplanationCont": "Es zielt darauf ab, Ihre tägliche Softwareentwicklung zu vereinfachen, ohne Sie daran zu hindern, Low-Level-Code zu schreiben.", + "SeeAllFeatures": "Alle Features anzeigen", + "CLI_CommandLineInterface": "CLI (Command Line Interface)", + "CLI_CommandLineInterfaceExplanation": "Enthält eine CLI, mit der Sie die Erstellung neuer Projekte und das Hinzufügen neuer Module automatisieren können.", + "StartupTemplates": "Startvorlagen", + "StartupTemplatesExplanation": "Verschiedene Startvorlagen bieten eine vollständig konfigurierte Lösung, um Ihre Entwicklung zu beschleunigen.", + "BasedOnFamiliarTools": "Basierend auf vertrauten Tools", + "BasedOnFamiliarToolsExplanation": "Aufbauend auf und integriert mit beliebten Tools, die Sie bereits kennen. Geringe Lernkurve, einfache Anpassung, komfortable Entwicklung.", + "ORMIndependent": "ORM-unabhängig", + "ORMIndependentExplanation": "Das Kernframework ist ORM-/datenbankunabhängig und kann mit jeder Datenquelle arbeiten. Entity Framework Core- und MongoDB-Anbieter sind bereits verfügbar.", + "Features": "Entdecken Sie die ABP Framework-Features", + "ABPCLI": "ABP CLI", + "Modularity": "Modularität", + "BootstrapTagHelpers": "Bootstrap Tag Helpers", + "DynamicForms": "Dynamische Formulare", + "BundlingMinification": "Bundling & Minification", + "BackgroundJobs": "Background Jobs", + "BackgroundJobsExplanation": "Definieren Sie einfache Klassen, um Jobs im Hintergrund in der Warteschlange auszuführen. Verwenden Sie den integrierten Jobmanager oder integrieren Sie Ihren eigenen. Hangfire & RabbitMQ -Integrationen sind bereits verfügbar.", + "DDDInfrastructure": "DDD-Infrastruktur", + "DomainDrivenDesignInfrastructure": "Domain Driven Design-Infrastruktur", + "AutoRESTAPIs": "Auto REST APIs", + "DynamicClientProxies": "Dynamische Client-Proxies", + "DistributedEventBus": "Distributed Event Bus", + "DistributedEventBusWithRabbitMQIntegration": "Distributed Event Bus mit RabbitMQ-Integration", + "TestInfrastructure": "Test-Infrastruktur", + "AuditLoggingEntityHistories": "Audit Logging & Entity Histories", + "ObjectToObjectMapping": "Object to Object Mapping", + "ObjectToObjectMappingExplanation": " Object to Object Mapping Abstraktion mit AutoMapper-Integration.", + "EmailSMSAbstractions": "E-Mail & SMS Abstraktionen", + "EmailSMSAbstractionsWithTemplatingSupport": "E-Mail- und SMS-Abstraktionen mit Vorlagenunterstützung", + "Localization": "Lokalisierung", + "SettingManagement": "Einstellungsverwaltung", + "ExtensionMethods": "Erweiterungsmethoden", + "ExtensionMethodsHelpers": "Erweiterungsmethoden & Helfer", + "AspectOrientedProgramming": "Aspektorientierte Programmierung", + "DependencyInjection": "Dependency Injection", + "DependencyInjectionByConventions": "Dependency Injection durch Konventionen", + "ABPCLIExplanation": "ABP CLI (Command Line Interface) ist ein Befehlszeilenprogramm zum Ausführen einiger gängiger Vorgänge für ABP-basierte Lösungen.", + "ModularityExplanation": "ABP bietet eine vollständige Infrastruktur zum Erstellen eigener Anwendungsmodule, die Entities, Services, Datenbankintegration, APIs, UI-Komponenten usw. enthalten können.", + "MultiTenancyExplanation": "Das ABP-Framework unterstützt nicht nur die Entwicklung von Multi-Mandantenanwendungen, sondern macht Ihren Code von der Mandantenfähigkeit auch weitgehend unabhängig.", + "MultiTenancyExplanation2": "Kann den aktuellen Mandanten automatisch ermitteln und Daten verschiedener Mandanten voneinander isolieren.", + "MultiTenancyExplanation3": "Unterstützt einzelne Datenbank-, Datenbank-pro-Mandanten- und Hybrid-Ansätze.", + "MultiTenancyExplanation4": "Sie konzentrieren sich auf Ihren geschäftsspezifischen Code und lassen das Framework die Mandantenfähigkeit für Sie übernehmen.", + "BootstrapTagHelpersExplanation": "Anstatt die sich wiederholenden Details von Bootstrap-Komponenten manuell zu schreiben, verwenden Sie die Tag-Helper von ABP, um diese zu vereinfachen und dabei die Vorteile von Intellisense zu nutzen. Sie können Bootstrap weitherhin verwenden, wann immer Sie es benötigen.", + "DynamicFormsExplanation": "Helfer für dynamische Formular- und Input-Tags können das vollständige Formular anhand einer C#-Klasse als Model erstellen.", + "AuthenticationAuthorizationExplanation": "Umfangreiche Authentifizierungs- und Autorisierungsoptionen, die in ASP.NET Core Identity & IdentityServer4 integriert sind. Bietet ein erweiterbares und detailliertes Berechtigungssystem.", + "CrossCuttingConcernsExplanation": "Wiederholen Sie sich nicht, um all diese allgemeinen Dinge immer wieder zu implementieren. Konzentrieren Sie sich auf Ihren geschäftsspezifischen Code und lassen Sie ihn von ABP durch Konventionen automatisieren.", + "DatabaseConnectionTransactionManagement": "Datenbankverbindungs- und Transaktionsmanagement", + "CorrelationIdTracking": "Correlation-ID-Verfolgung", + "BundlingMinificationExplanation": "ABP bietet ein einfaches, dynamisches, leistungsstarkes, modulares und integriertes Bundling- und Minification-System.", + "VirtualFileSystemnExplanation": "Das virtuelle Dateisystem ermöglicht die Verwaltung von Dateien, die physisch nicht auf dem Dateisystem (Datenträger) vorhanden sind. Es wird hauptsächlich verwendet, um Dateien (js, css, image, cshtml ...) in Assemblys einzubetten und sie zur Laufzeit wie physische Dateien zu verwenden.", + "ThemingExplanation": "Mit dem Theming-System können Sie Ihre Anwendung & Module Theme-unabhängig entwickeln, indem Sie eine Reihe gemeinsamer Basisbibliotheken und Layouts definieren, die auf dem neuesten Bootstrap-Framework basieren.", + "DomainDrivenDesignInfrastructureExplanation": "Eine vollständige Infrastruktur zum Erstellen von mehrschichtigen Anwendungen basierend auf den Domain Driven Design Entwurfsmustern und -prinzipien;", + "Specification": "Specification", + "Repository": "Repository", + "DomainService": "Domain Service", + "ValueObject": "Value Object", + "ApplicationService": "Application Service", + "DataTransferObject": "Data Transfer Object", + "AggregateRootEntity": "Aggregate Root, Entity", + "AutoRESTAPIsExplanation": "ABP kann Ihre Anwendungsservices gemäß Konvention automatisch als API-Controller konfigurieren.", + "DynamicClientProxiesExplanation": "Verwenden Sie Ihre APIs ganz einfach in JavaScript- und C#-Clients.", + "DistributedEventBusWithRabbitMQIntegrationExplanation": "Veröffentlichen und konsumieren Sie Distributed Events einfach mithilfe des integrierten Distributed Event Bus mit verfügbarer RabbitMQ-Integration.", + "TestInfrastructureExplanation": "Das Framework wurde unter Berücksichtigung von Unit- und Integrationstests entwickelt. Bietet Ihnen Basisklassen, um es einfacher zu machen. Startvorlagen werden mit vorkonfiguriert Tests geliefert.", + "AuditLoggingEntityHistoriesExplanation": "Integriertes Audit Logging für geschäftskritische Anwendungen. Audit Logging auf Request-, Service-, und Methodenebene sowie Entity-Historien mit Details auf Property-Ebene.", + "EmailSMSAbstractionsWithTemplatingSupportExplanation": "IEmailSender- und ISmsSender-Abstraktionen entkoppeln Ihre Anwendungslogik von der Infrastruktur. Das erweiterte E-Mail-Vorlagensystem ermöglicht das Erstellen und Lokalisieren von E-Mail-Vorlagen und deren einfache Verwendung bei Bedarf.", + "LocalizationExplanation": "Das Lokalisierungssystem ermöglicht das Erstellen von Ressourcen in einfachen JSON-Dateien und die Lokalisierung Ihrer Benutzeroberfläche. Es unterstützt erweiterte Szenarien wie Vererbung, Erweiterungen und JavaScript-Integration und ist vollständig mit dem Lokalisierungssystem von AspNet Core kompatibel.", + "SettingManagementExplanation": "Definieren Sie Einstellungen für Ihre Anwendung und erhalten Sie zur Laufzeit Werte basierend auf der aktuellen Konfiguration, dem Mandanten und dem Benutzer.", + "ExtensionMethodsHelpersExplanation": "Wiederholen Sie sich nicht einmal für triviale Codeteile. Erweiterungen und Helfer für Standardtypen machen Ihren Code viel sauberer und einfacher zu schreiben.", + "AspectOrientedProgrammingExplanation": "Bietet eine komfortable Infrastruktur zum Erstellen dynamischer Proxys und zum Implementieren der aspektorientierten Programmierung. Fangen Sie eine Klasse ab und führen Sie Ihren Code vor und nach jeder Methodenausführung aus.", + "DependencyInjectionByConventionsExplanation": "Sie müssen Ihre Klassen nicht manuell für die Dependency Injection registrieren. Registriert gängige Servicetypen automatisch gemäß Konvention. Für andere Arten von Services können Sie Schnittstellen und Attribute verwenden, um dies einfacher gestalten und an Ort und Stelle zu ermöglichen.", + "DataFilteringExplanation": "Definieren und verwenden Sie Datenfilter, die automatisch angewendet werden, wenn Sie Entities aus der Datenbank abfragen. Soft Delete- und Multimandanten-Filter sind sofort verfügbar, wenn Sie einfache Schnittstellen implementieren.", + "PublishEvents": "Events veröffentlichen", + "HandleEvents": "Auf Events reagieren", + "AndMore": "und mehr...", + "Code": "Code", + "Result": "Resultat", + "SeeTheDocumentForMoreInformation": "Weitere Informationen finden Sie in der {0} -Dokumentation ", + "IndexPageHeroSection": "Open SourceWebanwendung
Framework
für ASP.Net Core", + "UiFramework": "UI-Framework", + "EmailAddress": "E-Mail-Adresse", + "Mobile": "Mobile", + "ReactNative": "React Native", + "Strong": "Stark", + "Complete": "Vollständig", + "BasedLayeringModel": "Based Layering Model", + "Microservice": "Microservice", + "Compatible": "Kompatibel", + "MeeTTheABPCommunityInfo": "Unsere Mission ist es, eine Umgebung zu schaffen, in der Entwickler sich gegenseitig mit Beiträgen, Tutorials, Fallstudien usw. helfen und Gleichgesinnte treffen können.", + "JoinTheABPCommunityInfo": "Beteiligen Sie sich an einer lebendigen Community und tragen Sie zum ABP Framework bei!", + "AllArticles": "Alle Beiträge", + "SubmitYourArticle": "Reichen Sie Ihren Beitrag ein", + "DynamicClientProxyDocument": "In der Dokumentation zu den Dynamischen Client-Proxies finden Sie Informationen zu JavaScript & C#.", + "EmailSMSAbstractionsDocument": "Weitere Informationen finden Sie in den Unterlagen E-Mail-Senden and SMS-Senden.", + "CreateProjectWizard": "Dieser Assistent erstellt ein neues Projekt aus der Startvorlage, die ordnungsgemäß konfiguriert ist, um Ihr Projekt zu starten.", + "TieredOption": "Erstellt eine Tiered Lösung, bei der Web- und HTTP-API-Ebenen physisch getrennt sind. Wenn diese Option nicht aktiviert ist, wird eine mehrschichtige Lösung erstellt, die weniger komplex und für die meisten Szenarien geeignet ist.", + "SeparateIdentityServerOption": "Trennt die Serverseite in zwei Anwendungen: Die erste ist für den Identitätsserver und die zweite für die serverseitige HTTP-API.", + "UseslatestPreVersion": "Verwendet die neueste Vorabversion", + "ReadTheDocumentation": "Lesen SieDie Dokumentation", + "Documentation": "Dokumentation", + "GettingStartedTutorial": "Erste Schritte Tutorial", + "ApplicationDevelopmentTutorial": "Tutorial zur Anwendungsentwicklung", + "TheStartupTemplate": "Die Startvorlage", + "InstallABPCLIInfo": "ABP CLI ist der schnellste Weg, um eine neue Lösung mit dem ABP-Framework zu starten. Installieren Sie die ABP-CLI über die Eingabeaufforderung:", + "DifferentLevelOfNamespaces": "Sie können verschiedene Ebenen von Namespaces verwenden; z.B. BookStore, Acme.BookStore or Acme.Retail.BookStore.", + "ABPCLIExamplesInfo": "Der Befehl new erstellt eine mehrschichtige MVC-Anwendung mit Entity Framework Core als Datenbankanbieter. Es gibt jedoch zusätzliche Optionen. Beispiele:", + "SeeCliDocumentForMoreInformation": "Weitere Optionen finden Sie im ABP CLI-Dokument oder wählen Sie oben die Registerkarte \"Direkter Download\".", + "Optional": "Optional", + "LocalFrameworkRef": "Behalten Sie die lokale Projektreferenz für die Framework-Pakete bei.", + "SelectLanguage": "Sprache auswählen" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en-GB.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en-GB.json new file mode 100644 index 0000000000..cd0592524d --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en-GB.json @@ -0,0 +1,198 @@ +{ + "culture": "en-GB", + "texts": { + "GetStarted": "Get Started - Startup Templates", + "Create": "Create", + "NewProject": "New Project", + "DirectDownload": "Direct Download", + "ProjectName": "Project name", + "ProjectType": "Project type", + "DatabaseProvider": "Database provider", + "NTier": "N-Tier", + "IncludeUserInterface": "Include user interface", + "CreateNow": "Create now", + "TheStartupProject": "The startup project", + "Tutorial": "Tutorial", + "UsingCLI": "Using CLI", + "SeeDetails": "See Details", + "AbpShortDescription": "ABP Framework is a complete infrastructure to create modern web applications by following the software development best practices and conventions.", + "SourceCodeUpper": "SOURCE CODE", + "LatestReleaseLogs": "Latest release logs", + "Infrastructure": "Infrastructure", + "Architecture": "Architecture", + "Modular": "Modular", + "DontRepeatYourself": "Don’t Repeat Yourself", + "DeveloperFocused": "Developer Focused", + "FullStackApplicationInfrastructure": "Full stack application infrastructure.", + "DomainDrivenDesign": "Domain Driven Design", + "DomainDrivenDesignExplanation": "Designed and developed based on DDD patterns and principles. Provides a layered model for your application.", + "Authorization": "Authorisation", + "AuthorizationExplanation": "Advanced authorisation with user, role and fine-grained permission system. Built on the Microsoft Identity library.", + "MultiTenancy": "Multi-Tenancy", + "MultiTenancyExplanationShort": "SaaS applications made easy! Integrated multi-tenancy from database to UI.", + "CrossCuttingConcerns": "Cross Cutting Concerns", + "CrossCuttingConcernsExplanationShort": "Complete infrastructure for authorization, validation, exception handling, caching, audit logging, transaction management and more.", + "BuiltInBundlingMinification": "Built-In Bundling & Minification", + "BuiltInBundlingMinificationExplanation": "No need to use external tools for bundling & minification. ABP offers a simpler, dynamic, powerful, modular and built-in way!", + "VirtualFileSystem": "Virtual File System", + "VirtualFileSystemExplanation": "Embed views, scripts, styles, images... into packages/libraries and reuse them in different applications.", + "Theming": "Theming", + "ThemingExplanationShort": "Use and customise the bootstrap-based standard UI theme or create your own.", + "BootstrapTagHelpersDynamicForms": "Bootstrap Tag Helpers & Dynamic Forms", + "BootstrapTagHelpersDynamicFormsExplanation": "Instead of manually writing bootstrap components, Use ABP's tag helpers to simplify and take advantage of intellisense. Quickly build UI forms based on a C# model using the dynamic form tag helper.", + "HTTPAPIsDynamicProxies": "HTTP APIs & Dynamic Proxies", + "HTTPAPIsDynamicProxiesExplanation": "Automatically expose application services as REST style HTTP APIs, and consume them with dynamic JavaScript and C# proxies.", + "CompleteArchitectureInfo": "Modern architecture to create maintainable software solutions.", + "DomainDrivenDesignBasedLayeringModelExplanation": "Helps you to implement a DDD based layered architecture and build a maintainable code base.", + "DomainDrivenDesignBasedLayeringModelExplanationCont": "Provides startup templates, abstractions, base classes, services, documentation and guides to help you to develop your application based on DDD patterns & principles.", + "MicroserviceCompatibleModelExplanation": "The core framework & pre-build modules are designed with microservice architecture in mind.", + "MicroserviceCompatibleModelExplanationCont": "Provides infrastructure, integrations, samples and documentation to implement microservice solutions easier, while it doesn’t bring additional complexity if you want a monolithic application.", + "ModularInfo": "ABP provides a module system that allows you to develop reusable application modules, tie into application lifecycle events, and express dependencies between core parts of your system.", + "PreBuiltModulesThemes": "Pre-Built Modules & Themes", + "PreBuiltModulesThemesExplanation": "Open source and commercial modules & themes are ready to use in your business application.", + "NuGetNPMPackages": "NuGet & NPM Packages", + "NuGetNPMPackagesExplanation": "Distributed as NuGet & NPM packages. Easy to install and upgrade.", + "ExtensibleReplaceable": "Extensible/Replaceable", + "ExtensibleReplaceableExplanation": "All services & modules are designed extensibility in mind. You can replace services, pages, styles and components.", + "CrossCuttingConcernsExplanation2": "Keep your codebase smaller so you can maintain focus on the code that’s specific to your business.", + "CrossCuttingConcernsExplanation3": "Don’t spend time implementing common application requirements on multiple projects.", + "AuthenticationAuthorization": "Authentication & Authorization", + "ExceptionHandling": "Exception Handling", + "Validation": "Validation", + "DatabaseConnection": "Database Connection", + "TransactionManagement": "Transaction management", + "AuditLogging": "Audit Logging", + "Caching": "Caching", + "Multitenancy": "Multitenancy", + "DataFiltering": "Data filtering", + "ConventionOverConfiguration": "Convention Over Configuration", + "ConventionOverConfigurationExplanation": "ABP implements common application conventions by default with a minimal or zero configuration.", + "ConventionOverConfigurationExplanationList1": "Auto registers known services to dependency injection.", + "ConventionOverConfigurationExplanationList2": "Exposes application services as HTTP APIs by naming conventions.", + "ConventionOverConfigurationExplanationList3": "Creates dynamic HTTP client proxies for C# and JavaScript.", + "ConventionOverConfigurationExplanationList4": "Provides default repositories for your entities.", + "ConventionOverConfigurationExplanationList5": "Manages Unit of Work per web request or application service method.", + "ConventionOverConfigurationExplanationList6": "Publishes create, update & delete events for your entities.", + "BaseClasses": "Base Classes", + "BaseClassesExplanation": "Pre-built base classes for common application patterns.", + "DeveloperFocusedExplanation": "ABP is for developers.", + "DeveloperFocusedExplanationCont": "It aims to simplify your daily software development while not restricting you from writing low level code.", + "SeeAllFeatures": "See All Features", + "CLI_CommandLineInterface": "CLI (Command Line Interface)", + "CLI_CommandLineInterfaceExplanation": "Includes a CLI to help you automate the creation of new projects and the addition of new modules.", + "StartupTemplates": "Startup Templates", + "StartupTemplatesExplanation": "Various startup templates provide a fully configured solution to jump start your development.", + "BasedOnFamiliarTools": "Based on Familiar Tools", + "BasedOnFamiliarToolsExplanation": "Built and integrated with popular tools you already know. Low learning curve, easy adaptation, comfortable development.", + "ORMIndependent": "ORM Independent", + "ORMIndependentExplanation": "The core framework is ORM/database independent and can work with any data source. Entity Framework Core and MongoDB providers are already available.", + "Features": "Explore the ABP Framework Features", + "ABPCLI": "ABP CLI", + "Modularity": "Modularity", + "BootstrapTagHelpers": "Bootstrap Tag Helpers", + "DynamicForms": "Dynamic Forms", + "BundlingMinification": "Bundling & Minification", + "BackgroundJobs": "Background Jobs", + "BackgroundJobsExplanation": "Define simple classes to execute jobs in the background as queued. Use the built-in job manager or integrate your own. Hangfire & RabbitMQ integrations are already available.", + "DDDInfrastructure": "DDD Infrastructure", + "DomainDrivenDesignInfrastructure": "Domain Driven Design Infrastructure", + "AutoRESTAPIs": "Auto REST APIs", + "DynamicClientProxies": "Dynamic Client Proxies", + "DistributedEventBus": "Distributed Event Bus", + "DistributedEventBusWithRabbitMQIntegration": "Distributed Event Bus with RabbitMQ Integration", + "TestInfrastructure": "Test Infrastructure", + "AuditLoggingEntityHistories": "Audit Logging & Entity Histories", + "ObjectToObjectMapping": "Object to Object Mapping", + "ObjectToObjectMappingExplanation": "Object to object mapping abstraction with AutoMapper integration.", + "EmailSMSAbstractions": "Email & SMS Abstractions", + "EmailSMSAbstractionsWithTemplatingSupport": "Email & SMS Abstractions with Templating Support", + "Localization": "Localization", + "SettingManagement": "Setting Management", + "ExtensionMethods": "Extension Methods", + "ExtensionMethodsHelpers": "Extension Methods & Helpers", + "AspectOrientedProgramming": "Aspect Oriented Programming", + "DependencyInjection": "Dependency Injection", + "DependencyInjectionByConventions": "Dependency Injection by Conventions", + "ABPCLIExplanation": "The ABP CLI (Command Line Interface) is a command line tool to perform common operations for ABP based solutions.", + "ModularityExplanation": "ABP provides a complete infrastructure to build your own application modules those may have entities, services, database integration, APIs, UI components and so on.", + "MultiTenancyExplanation": "ABP framework not only supports to develop multi-tenant applications, but also makes your code mostly unaware of the multi-tenancy.", + "MultiTenancyExplanation2": "Can automatically determine the current Tenant and isolate each Tenant's data from each other.", + "MultiTenancyExplanation3": "Supports single database, database per tenant and hybrid approaches.", + "MultiTenancyExplanation4": "You focus on your business code and let the framework to handle multi-tenancy on behalf of you.", + "BootstrapTagHelpersExplanation": "Instead of manually writing the repeating details of bootstrap components, use ABP's tag helpers to simplify and take advantage of intellisense. You can use Bootstrap whenever you need it.", + "DynamicFormsExplanation": "Dynamic form & input tag helpers can create the complete form from a C# class as the model.", + "AuthenticationAuthorizationExplanation": "Rich authentication & authorization options integrated to ASP.NET Core Identity & IdentityServer4. Provides an extensible & detailed permission system.", + "CrossCuttingConcernsExplanation": "Don't repeat yourself when implementing common features. Instead, focus on your business logic and let ABP automate by convention.", + "DatabaseConnectionTransactionManagement": "Database Connection & Transaction Management", + "CorrelationIdTracking": "Correlation-Id Tracking", + "BundlingMinificationExplanation": "ABP offers a simple, dynamic, powerful, modular and built-in bundling & minification system.", + "VirtualFileSystemnExplanation": "The Virtual File System makes it possible to manage files those do not physically exist on the file system (disk). It's mainly used to embed (js, css, image, cshtml...) files into assemblies and use them like physical files on runtime.", + "ThemingExplanation": "Theming system allows to develop your application & modules theme independent by defining a set of common base libraries and layouts, based on the latest Bootstrap framework.", + "DomainDrivenDesignInfrastructureExplanation": "A complete infrastructure to build layered applications based on the Domain Driven Design patterns & principles.", + "Specification": "Specification", + "Repository": "Repository", + "DomainService": "Domain Service", + "ValueObject": "Value Object", + "ApplicationService": "Application Service", + "DataTransferObject": "Data Transfer Object", + "AggregateRootEntity": "Aggregate Root, Entity", + "AutoRESTAPIsExplanation": "ABP can automagically configure your application services as API Controllers by convention.", + "DynamicClientProxiesExplanation": "Easily consume your APIs from JavaScript and C# clients.", + "DistributedEventBusWithRabbitMQIntegrationExplanation": "Easily publish & consume distributed events using built-in Distributed Event Bus with RabbitMQ integration available.", + "TestInfrastructureExplanation": "The framework has been developed unit & integration testing in mind. Provides you base classes to make it easier. Startup templates come with pre-configured for testing.", + "AuditLoggingEntityHistoriesExplanation": "Built-in audit logging for business-critical applications. Request, service, method level audit logging and entity histories with property-level details.", + "EmailSMSAbstractionsWithTemplatingSupportExplanation": "IEmailSender and ISmsSender abstractions decouples your application logic from the infrastructure. Advanced email template system allows to create & localize email templates and easily use whenever needed.", + "LocalizationExplanation": "Localization system allows to create resources in plain JSON files and use them to localize your UI. It supports advanced scenarios like inheritance, extensions and JavaScript integration while it is fully compatible with AspNet Core's localization system.", + "SettingManagementExplanation": "Define settings for your application and get values on runtime based on the current configuration, tenant and user.", + "ExtensionMethodsHelpersExplanation": "Don't repeat yourself even for trivial code parts. Extensions & helpers for standard types makes your code much cleaner and easy to write.", + "AspectOrientedProgrammingExplanation": "Provides a comfortable infrastructure to create dynamic proxies and implement Aspect Oriented Programming. Intercept any class and execute your code before & after every method execution.", + "DependencyInjectionByConventionsExplanation": "No need to register your classes to dependency injection manually. Automatically registers common service types by convention. For other type of services, you can use interfaces and attributes to make it easier and in-place.", + "DataFilteringExplanation": "Define and use data filters those are automatically applied when you query entities from database. Soft Delete & Multi-Tenant filters are provided out of the box when you implement simple interfaces.", + "PublishEvents": "Publish Events", + "HandleEvents": "Handle Events", + "AndMore": "and more...", + "Code": "Code", + "Result": "Result", + "SeeTheDocumentForMoreInformation": "See the {0} document for more information", + "IndexPageHeroSection": "open sourceWeb Application
Framework
for asp.net core", + "UiFramework": "UI Framework", + "EmailAddress": "Email address", + "Mobile": "Mobile", + "ReactNative": "React Native", + "Strong": "Strong", + "Complete": "Complete", + "BasedLayeringModel": "Based Layering Model", + "Microservice": "Microservice", + "Compatible": "Compatible", + "MeeTTheABPCommunityInfo": "Our mission is to create an environment where developers can help each other with articles, tutorials, case studies, etc. and meet like-minded people.", + "JoinTheABPCommunityInfo": "Get involved with a vibrant community and become a contributor to the ABP Framework!", + "AllArticles": "All Articles", + "SubmitYourArticle": "Submit Your Article", + "DynamicClientProxyDocument": "See the dynamic client proxy documentations for JavaScript & C#.", + "EmailSMSAbstractionsDocument": "See the emailing and SMS sending documents for more information.", + "CreateProjectWizard": "This wizard creates a new project from the startup template which is properly configured to jump start to your project.", + "TieredOption": "Creates a tiered solution where Web and Http API layers are physically separated. If not checked, creates a layered solution which is less complex and suitable for most scenarios.", + "SeparateIdentityServerOption": "Separates server side into two applications: First one is for the identity server and the second one is for your server side HTTP API.", + "UseslatestPreVersion": "Uses latest pre-release version", + "ReadTheDocumentation": "ReadThe Documentation", + "Documentation": "Documentation", + "GettingStartedTutorial": "Getting Started Tutorial", + "ApplicationDevelopmentTutorial": "Application Development Tutorial", + "TheStartupTemplate": "The Startup Template", + "InstallABPCLIInfo": "ABP CLI is the fastest way to start a new solution with the ABP framework. Install the ABP CLI using a command line window:", + "DifferentLevelOfNamespaces": "You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.", + "ABPCLIExamplesInfo": "new command creates a layered MVC application with Entity Framework Core as the database provider. However, it has additional options. Examples:", + "SeeCliDocumentForMoreInformation": "See the ABP CLI document for more options or select the \"Direct Download\" tab above.", + "Optional": "Optional", + "LocalFrameworkRef": "Keep local project reference for the framework packages.", + "BlobStoring": "BLOB Storing", + "BlobStoringExplanation": "BLOB Storing system provides an abstraction to work with BLOBs. ABP provides some pre-built storage provider integrations (Azure, AWS, File System, Database, etc.) that you can easily use in your applications.", + "TextTemplating": "Text Templating", + "TextTemplatingExplanation": "Text templating is used to dynamically render contents based on a template and a model (a data object). For example, you can use it to create dynamic email contents with a pre-built template.", + "MultipleUIOptions": "Multiple UI Options", + "MultipleDBOptions": "Multiple Database Providers", + "MultipleUIOptionsExplanation": "The core framework is designed as UI independent and can work with any type of UI system, while there are multiple pre-built and integrated options are provided out of the box.", + "MultipleDBOptionsExplanation": "The framework can work with any data source, while the following providers are officially developed and supported:", + "SelectLanguage": "Select language" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index cb4a8d076c..bdc7364788 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -45,7 +45,7 @@ "CompleteArchitectureInfo": "Modern architecture to create maintainable software solutions.", "DomainDrivenDesignBasedLayeringModelExplanation": "Helps you to implement a DDD based layered architecture and build a maintainable code base.", "DomainDrivenDesignBasedLayeringModelExplanationCont": "Provides startup templates, abstractions, base classes, services, documentation and guides to help you to develop your application based on DDD patterns & principles.", - "MicroserviceCompatibleModelExplanation": "The core framework & pre-build modules are designed the microservice architecture in mind.", + "MicroserviceCompatibleModelExplanation": "The core framework & pre-build modules are designed with microservice architecture in mind.", "MicroserviceCompatibleModelExplanationCont": "Provides infrastructure, integrations, samples and documentation to implement microservice solutions easier, while it doesn\u2019t bring additional complexity if you want a monolithic application.", "ModularInfo": "ABP provides a module system that allows you to develop reusable application modules, tie into application lifecycle events, and express dependencies between core parts of your system.", "PreBuiltModulesThemes": "Pre-Built Modules & Themes", @@ -184,6 +184,15 @@ "ABPCLIExamplesInfo": "new command creates a layered MVC application with Entity Framework Core as the database provider. However, it has additional options. Examples:", "SeeCliDocumentForMoreInformation": "See the ABP CLI document for more options or select the \"Direct Download\" tab above.", "Optional": "Optional", - "LocalFrameworkRef": "Keep local project reference for the framework packages." + "LocalFrameworkRef": "Keep local project reference for the framework packages.", + "BlobStoring": "BLOB Storing", + "BlobStoringExplanation": "BLOB Storing system provides an abstraction to work with BLOBs. ABP provides some pre-built storage provider integrations (Azure, AWS, File System, Database, etc.) that you can easily use in your applications.", + "TextTemplating": "Text Templating", + "TextTemplatingExplanation": "Text templating is used to dynamically render contents based on a template and a model (a data object). For example, you can use it to create dynamic email contents with a pre-built template.", + "MultipleUIOptions": "Multiple UI Options", + "MultipleDBOptions": "Multiple Database Providers", + "MultipleUIOptionsExplanation": "The core framework is designed as UI independent and can work with any type of UI system, while there are multiple pre-built and integrated options are provided out of the box.", + "MultipleDBOptionsExplanation": "The framework can work with any data source, while the following providers are officially developed and supported;", + "SelectLanguage": "Select language" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/es.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/es.json new file mode 100644 index 0000000000..06d197d82e --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/es.json @@ -0,0 +1,190 @@ +{ + "culture": "es", + "texts": { + "GetStarted": "Comencemos - Plantillas de inicio", + "Create": "Crear", + "NewProject": "Nuevo proyecto", + "DirectDownload": "Descarga directa", + "ProjectName": "Nombre de proyecto", + "ProjectType": "Tipo de proyecto", + "DatabaseProvider": "Proveedor de base de datos", + "NTier": "N-Capas", + "IncludeUserInterface": "Incluir interface de usuario", + "CreateNow": "Crear ahora", + "TheStartupProject": "El proyecto de inicio", + "Tutorial": "Tutorial", + "UsingCLI": "Usando CLI", + "SeeDetails": "Ver detalles", + "AbpShortDescription": "ABP Framework es una completa infraestructure para crear modernas aplicaciones web que sigue las mejores prácticas y convenciones de desarrollo de software.", + "SourceCodeUpper": "CÓDIGO FUENTE", + "LatestReleaseLogs": "Últimos registros de lanzamiento", + "Infrastructure": "infraestructura", + "Architecture": "Arquitectura", + "Modular": "Modular", + "DontRepeatYourself": "No te repitas tu mismo", + "DeveloperFocused": "Pensado para el desarrollador", + "FullStackApplicationInfrastructure": "infraestructura de aplicación Full stack", + "DomainDrivenDesign": "", + "DomainDrivenDesignExplanation": "Diseñado y desarrollado basandose en los patrones y principios de DDD. Promorciona una capa de modelo para tus aplicaciones.", + "Authorization": "Autorización", + "AuthorizationExplanation": "Sistema de autorización avanzado con usuarios, roles y permisos de granularidad-fina. Construido sobre la librería Microsoft Identity.", + "MultiTenancy": "Multi-inquilino", + "MultiTenancyExplanationShort": "Aplicaciones SaaS sencillas!, integradas con multi-inquilino desde la base de datos hasta la UI.", + "CrossCuttingConcerns": "Requerimientos comunes", + "CrossCuttingConcernsExplanationShort": "Completa infraestructura de autorización, validación, manejo de excepciones, caching, auditoría, gestión de transacciones y más.", + "BuiltInBundlingMinification": "Empaquetado y minificado integrado", + "BuiltInBundlingMinificationExplanation": "No se necesitan usar herramientas externas para el empaquetado y minificado. ABP ofrece un camino simple, dinámico, potente, modular e integrado.", + "VirtualFileSystem": "Sistema de ficheros virtual", + "VirtualFileSystemExplanation": "Incrusta vistas, scripts, estilos, imágenes en paquetes/ librerias y reusalos en diferentes aplicaciones.", + "Theming": "Temas", + "ThemingExplanationShort": "Usa y personaliza la UI estandar basada en bootstrap o crea tu propio tema.", + "BootstrapTagHelpersDynamicForms": "Boostrap Tag Helpers y formularios dinámicos", + "BootstrapTagHelpersDynamicFormsExplanation": "", + "HTTPAPIsDynamicProxies": "HTTP APIs y proxies dinámicos", + "HTTPAPIsDynamicProxiesExplanation": "Expon servicios de aplicación como REST HTPP APIs y consumelos dinámicamente desde proxies JavaScript y C#.", + "CompleteArchitectureInfo": "Arquitectura moderna para crear soluciones software sostenibles.", + "DomainDrivenDesignBasedLayeringModelExplanation": "Ayuda a implementar ", + "DomainDrivenDesignBasedLayeringModelExplanationCont": "Promorciona plantillas de inicio, clases base, servicios, documentación y guías de ayuda para desarrollar tu aplicación basada en patrones y principios DDD.", + "MicroserviceCompatibleModelExplanation": "El núcleo del framework y los modules pre-construidos están diseñados con una arquitectura microservicios en mente.", + "MicroserviceCompatibleModelExplanationCont": "Proporciona la infraestructura, integraciones, ejemplos y documentación para implementar soluciones microservicios más facilmente, mientras no trae complejidad adicional si quieres una aplicación monolítica.", + "ModularInfo": "ABP proporciona un sistema de modulos que te permite desarrollar modulos de aplicacion resusables. Conectado en el ciclo de eventos de aplicación y mínimas dependencias entre las partes principales de tu sistema.", + "PreBuiltModulesThemes": "Modulos y temas integrados", + "PreBuiltModulesThemesExplanation": "Módulos y temas de código abierto y comerciales preparados para usar en tu aplicación de negocio.", + "NuGetNPMPackages": "Paquetes Nuget y NPM", + "NuGetNPMPackagesExplanation": "Paquetes distribuidos a través de Nuget y NPM para instalar y actualizar.", + "ExtensibleReplaceable": "Extensible/ Reemplazable", + "ExtensibleReplaceableExplanation": "Todos los serivios y módulos están diseñados para ser extensibles. Tu puedes reemplazar servicios, páginas, estilos y componentes.", + "CrossCuttingConcernsExplanation2": "Mantiene tu código base más pequeño y te permite enforcate en el código que es especifico en tu negocio.", + "CrossCuttingConcernsExplanation3": "No pierdas tiempo implementando requerimientos comunes de aplicación en múltiples proyectos.", + "AuthenticationAuthorization": "Autenticación y autorización", + "ExceptionHandling": "Manejo de excepciones.", + "Validation": "Validación", + "DatabaseConnection": "Conexión a la base de datos", + "TransactionManagement": "Gestión de transacciones", + "AuditLogging": "Registro de auditoría", + "Caching": "Caché", + "Multitenancy": "Multi-inquilino", + "DataFiltering": "Filtrado de datos", + "ConventionOverConfiguration": "Convención sobre la configuración", + "ConventionOverConfigurationExplanation": "ABP implementa comunes convenciones de aplicación por defecto con una mínima o nula configuración.", + "ConventionOverConfigurationExplanationList1": "Los registradores conocen los servicios para la inyección de dependencias.", + "ConventionOverConfigurationExplanationList2": "Expone servicios de aplicación como APIs HTTP mediante convenciones de nomenclatura.", + "ConventionOverConfigurationExplanationList3": "Crea clientes proxies HTTP dinámicos para C# y JavaScript.", + "ConventionOverConfigurationExplanationList4": "Proporciona repositorios por defecto para tus entidades.", + "ConventionOverConfigurationExplanationList5": "Maneja Unit of Work por cada petición web o metodo de servicio de aplicación.", + "ConventionOverConfigurationExplanationList6": "Publica, crea, actualiza y borra eventos para tus entidades.", + "BaseClasses": "Clase base", + "BaseClassesExplanation": "Clases base integradas para patrones comunes de aplicación.", + "DeveloperFocusedExplanation": "ABP es para desarrolladores.", + "DeveloperFocusedExplanationCont": "Su objetivo es simplificar su desarrollo diario de software sin restringirle la escritura de código de bajo nivel.", + "SeeAllFeatures": "Ver todas las características", + "CLI_CommandLineInterface": "CLI (Command Line Interface)", + "CLI_CommandLineInterfaceExplanation": "Incluye un CLI que te ayuda a automatizar la creación de nuevos proyectos y añadir nuevos módulos.", + "StartupTemplates": "Plantillas de inicio", + "StartupTemplatesExplanation": "Varias plantillas de inicio proporcionan una solución completamente configurada lista para comenzar el desarrollo.", + "BasedOnFamiliarTools": "Basado en herramientas familiares", + "BasedOnFamiliarToolsExplanation": "Construido y diseñado con herramientas populares que tu ya conoces. Baja curva de aprendizaje, facil adaptación y desarrollo confortable.", + "ORMIndependent": "Independiente del ORM", + "ORMIndependentExplanation": "El núcleo del core es independiente del ORM/ base de datos y puedes trabajar con cualquier fuente de datos. Entity Framework Core y MongoDB están actualmente disponibles.", + "Features": "Explora las características de ABP Framework", + "ABPCLI": "ABP CLI", + "Modularity": "Modularidad", + "BootstrapTagHelpers": "Bootstrap Tag Helpers", + "DynamicForms": "Formularios dinámicos", + "BundlingMinification": "Empaquetado y Minimificación", + "BackgroundJobs": "", + "BackgroundJobsExplanation": "Defina clases simples para ejecutar trabajos en segundo plano como en cola. Utilice el administrador de trabajos integrado o integre el suyo propio. Hangfire & RabbitMQ integraciones están actualmente disponibles.", + "DDDInfrastructure": "infraestructura DSS", + "DomainDrivenDesignInfrastructure": "infraestructura Domain Driven Design", + "AutoRESTAPIs": "Auto REST APIs", + "DynamicClientProxies": "Clientes proxies dinámicos", + "DistributedEventBus": "Bus de eventos distribuido", + "DistributedEventBusWithRabbitMQIntegration": "Bus de eventos distribuido con la integración RabbitMQ ", + "TestInfrastructure": "infraestructura de Test", + "AuditLoggingEntityHistories": "Registro de auditoría y historial de entidades", + "ObjectToObjectMapping": "Mapeado objeto a objeto", + "ObjectToObjectMappingExplanation": "Mapeado objeto a objeto abstracción integrada con AutoMapper.", + "EmailSMSAbstractions": "Abstracciones para Email y SMS", + "EmailSMSAbstractionsWithTemplatingSupport": "Abstracciones para Email y SMS con soporte para plantillas", + "Localization": "Localización", + "SettingManagement": "Gestión de la configuración", + "ExtensionMethods": "Extension Methods", + "ExtensionMethodsHelpers": "Extension Methods & Helpers", + "AspectOrientedProgramming": "Programación orientada a aspectos", + "DependencyInjection": "Inyección de dependencias", + "DependencyInjectionByConventions": "Inyección de dependencias por convenciones", + "ABPCLIExplanation": "ABP CLI (Command Line Interface) es una herramienta en línea de comandos que proporciona ciertas tareas comunes para soluciones basadas en ABP.", + "ModularityExplanation": "ABP proporciona una infraestructura completa para construir tu propios módulos de aplicación que pueden contener entidades, servicios, base de datos integradas, APIs, componentes de UI y otros...", + "MultiTenancyExplanation": "ABP framework no sólo soporta el desarrollo de aplicaciones multi-inquilino, si no también hace que tu código no tenga que preocuparse de multi-inquilino.", + "MultiTenancyExplanation2": "Puedes determinar automaticamente el inquilino actual, y aislar los datos de diferentes inquilinos entre sí. ", + "MultiTenancyExplanation3": "Soporta base de datos única, base de datos por inquilino y enfoques híbridos.", + "MultiTenancyExplanation4": "Enfocate en tu código y deja al framework que maneje multi-inquilino en tu nombre.", + "BootstrapTagHelpersExplanation": "En vez de escribir repetidamente los detalle de componentes bootstrap, usa ABP's tag helpers para simplificarlo y obtener ventaja de intellisence. Tu puedes definitivamente usar Bootstrp cuando tu lo necesites.", + "DynamicFormsExplanation": "Los formularios dinámicos y tag helpers de entrada pueden crear el formulario completo a partir de una clase de C# como modelo. ", + "AuthenticationAuthorizationExplanation": "Autenticación enriquecidad y opciones de autorización integradas con ASP.NET Core Identity & IdentityServer4. Proporciona un extensible y detallado sistema de permisos.", + "CrossCuttingConcernsExplanation": "No te repitas tu mismo al implementar todas esas cosas comunes una y otra vez. Enfocate en tu negocio y permite a ABP automatizarlos por convención.", + "DatabaseConnectionTransactionManagement": "Conexión de base de datos y gestión de transacciones", + "CorrelationIdTracking": "Correlation-Id de seguimiento", + "BundlingMinificationExplanation": "ABP ofrece un sistema simple, dinámico, potente, modular y empaquetado & minificación ", + "VirtualFileSystemnExplanation": "El sistema de archivos virtuales permite administrar archivos que no existen físicamente en el sistema de archivos (disco). Se utiliza principalmente para incrustar (js, css, image, cshtml...) en ensamblados y utilizarlos como archivos físicos en tiempo de ejecución.", + "ThemingExplanation": "El sistema de temas permite desarrollar el tema de su aplicación y módulos independiente mediante la definición de un conjunto de bibliotecas y diseños base comunes, basado en el último marco de Trabajo bootstrap.", + "DomainDrivenDesignInfrastructureExplanation": "Una infraestructura completa para crear aplicaciones en capas basadas en los patrones y principios DDD", + "Specification": "Especificación", + "Repository": "Repositorios", + "DomainService": "Servicio de dominio", + "ValueObject": "Objetos de valor", + "ApplicationService": "Servicio de aplicación", + "DataTransferObject": "Objetos de transferencia de datos", + "AggregateRootEntity": "Agregado ráiz, Entidad", + "AutoRESTAPIsExplanation": "ABP puede configurar automáticamente los servicios de aplicación como controladores de API por convención.", + "DynamicClientProxiesExplanation": "Consuma fácilmente sus API de clientes de JavaScript y C#", + "DistributedEventBusWithRabbitMQIntegrationExplanation": "Publique y consuma eventos distribuidos fácilmente mediante el bus de eventos distribuido integrado con la integración RabbitMQ disponible.", + "TestInfrastructureExplanation": "El framework se ha desarrollado unitariamente y pruebas de integración. Proporciona clases base para que sea más fácil. Las plantillas de inicio vienen preconfiguradas para las pruebas.", + "AuditLoggingEntityHistoriesExplanation": "Registro de auditoría integrado para aplicaciones críticas para el negocio. Registro de auditorías de solicitud, servicio, nivel de método e historiales de entidades con detalles de nivel de propiedad.", + "EmailSMSAbstractionsWithTemplatingSupportExplanation": "Las abstracciones IEmailSender e ISmsSender desacoplan la lógica de la aplicación de la infraestructura. El sistema avanzado de plantillas de correo electrónico permite crear y localizar plantillas de correo electrónico y utilizarlas fácilmente cuando sea necesario.", + "LocalizationExplanation": "El sistema de localización permite crear recursos en archivos JSON sin formato y utilizarlos para localizar la interfaz de usuario. Admite escenarios avanzados como herencia, extensiones e integración de JavaScript, mientras que es totalmente compatible con el sistema de localización de AspNet Core.", + "SettingManagementExplanation": "Defina la configuración de la aplicación y obtenga valores en tiempo de ejecución en función de la configuración actual, el inquilino y el usuario.", + "ExtensionMethodsHelpersExplanation": "No te repitas ni siquiera para partes de código triviales. Extensiones y ayudantes para tipos estándar hace que su código sea mucho más limpio y fácil de escribir.", + "AspectOrientedProgrammingExplanation": "Proporciona una infraestructura cómoda para crear servidores proxy dinámicos e implementar la programación orientada a aspectos. Interceptar cualquier clase y ejecutar el código antes y después de cada ejecución del método.", + "DependencyInjectionByConventionsExplanation": "No es necesario registrar las clases en la inserción de dependencias manualmente. Registra automáticamente los tipos de servicio comunes por convención. Para otro tipo de servicios, puede utilizar interfaces y atributos para que sea más fácil y in situ.", + "DataFilteringExplanation": "Definir y usar filtros de datos que se aplican automáticamente al consultar entidades desde la base de datos. Los filtros Soft Delete & MultiTenant se proporcionan de fábrica cuando se implementan interfaces sencillas.", + "PublishEvents": "Publicar eventos", + "HandleEvents": "Manejar eventos", + "AndMore": "y más...", + "Code": "Código", + "Result": "Resultado", + "SeeTheDocumentForMoreInformation": "Ver el {0} documento para más información.", + "IndexPageHeroSection": "Código fuente abiertoWeb Application
Framework
para asp.net core", + "UiFramework": "UI Framework", + "EmailAddress": "Dirección de correo", + "Mobile": "Móvil", + "ReactNative": "React Nativo", + "Strong": "Fuerte", + "Complete": "Completar", + "BasedLayeringModel": "Módelo basado en capas", + "Microservice": "Microservicio", + "Compatible": "Compatible", + "MeeTTheABPCommunityInfo": "Nuestra misión es crear un entorno en el que los desarrolladores puedan ayudarse entre sí con artículos, tutoriales, estudios de casos, etc. y conocer personas de ideas afines.", + "JoinTheABPCommunityInfo": "¡Participe en una comunidad vibrante y conviértase en colaborador del Marco ABP!", + "AllArticles": "Todos los artículos", + "SubmitYourArticle": "Envía tu artículo", + "DynamicClientProxyDocument": "Ver la documentación del cliente proxy dinámico para JavaScript & C#.", + "EmailSMSAbstractionsDocument": "Ver los documentos de emailing y envío SMS para más información.", + "CreateProjectWizard": "Este asistente crea un nuevo proyecto a partir de la plantilla de inicio que está correctamente configurado para comenzar con su proyecto.", + "TieredOption": "Crea una solución por niveles en la que las capas de API Web y HTTP están separadas físicamente. Si no se marca, crea una solución en capas que es menos compleja y adecuada para la mayoría de los escenarios.", + "SeparateIdentityServerOption": "Separa el lado del servidor en dos aplicaciones: la primera es para el servidor de identidad y la segunda es para la API HTTP del lado del servidor.", + "UseslatestPreVersion": "Usar la última versión pre-release", + "ReadTheDocumentation": "LeerLa Documentación", + "Documentation": "Documentación", + "GettingStartedTutorial": "Tutorial para iniciarse", + "ApplicationDevelopmentTutorial": "Tutorial de desarrollo de aplicación", + "TheStartupTemplate": "La Plantilla de Inicio", + "InstallABPCLIInfo": "ABP CLI es la forma más rápida de iniciar una nueva solución con el marco ABP. Instale la CLI de ABP mediante una ventana de línea de comandos:", + "DifferentLevelOfNamespaces": "Tu puedes usar diferentes niveles de espacio de nombres; ej. BookStore, Acme.BookStore or Acme.Retail.BookStore.", + "ABPCLIExamplesInfo": "nuevo comando crea una aplicación MVC por capas con Entity Framework Core como proveedor de base de datos. Sin embargo, tiene distintas opciones. Ejemplos:", + "SeeCliDocumentForMoreInformation": "Ver el documento ABP CLI para más opciones o selecciona la \"Direct Download\" pestaña de arriba.", + "Optional": "Opcional", + "LocalFrameworkRef": "Mantén la referencia al proyecto local para los paquetes del framework.", + "SelectLanguage": "Vali keel" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pt-BR.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pt-BR.json index 34debb8ec8..3dfd773204 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pt-BR.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pt-BR.json @@ -154,6 +154,7 @@ "SeeTheDocumentForMoreInformation": "Consulte o {0} documento para obter mais informações", "IndexPageHeroSection": "código abertoAplicativo da Web
Framework
para o ASP.NET Core", "UiFramework": "UI Framework", - "EmailAddress": "Endereço de email" + "EmailAddress": "Endereço de email", + "SelectLanguage": "Selecione o idioma" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json index 5f423e940b..ef4ecbdcac 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json @@ -156,6 +156,7 @@ "UiFramework": "UI Framework", "EmailAddress": "E-Posta Adresi", "Mobile": "Mobil", - "ReactNative": "React Native" + "ReactNative": "React Native", + "SelectLanguage": "Dil seçin" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json index 4dcb0cad51..d01d847716 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json @@ -184,6 +184,15 @@ "ABPCLIExamplesInfo": "new命令创建一个 分层的MVC应用程序 使用 Entity Framework Core 做为数据库提供程序. 它还有其他选项. 示例:", "SeeCliDocumentForMoreInformation": "参阅 ABP CLI 文档 获得更多选项或选择上方的 \"直接下载\" 标签.", "Optional": "可选的", - "LocalFrameworkRef": "保留框架包的本地项目引用." + "LocalFrameworkRef": "保留框架包的本地项目引用.", + "BlobStoring": "BLOB存储", + "BlobStoringExplanation": "BLOB存储系统提供了BLOB的抽象. ABP提供了一些预构建的存储提供程序集成(Azure,AWS,文件系统,数据库等),你可以轻松的在你的应用程序中使用它们.", + "TextTemplating": "文本模板", + "TextTemplatingExplanation": "文本模板是基于模板和模型(数据对象)使用动态渲染内容. 例如你可以使用预构建的模板来创建动态的电子邮件内容.", + "MultipleUIOptions": "多个UI选项", + "MultipleDBOptions": "多个数据库提供程序", + "MultipleUIOptionsExplanation": "核心框架设计为独立与UI,可以和任何类型的UI系统一起使用. 同时提供了多个开箱即用的预构建集成选项.", + "MultipleDBOptionsExplanation": "该框架可以使用任何数据源,并且以下提供程序已得到正式开发和支持;", + "SelectLanguage": "选择语言" } -} \ No newline at end of file +} diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hant.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hant.json index 777c9f6bbd..37f6747ea0 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hant.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hant.json @@ -154,6 +154,7 @@ "SeeTheDocumentForMoreInformation": "查看{0} 文件獲得更多訊息", "IndexPageHeroSection": "asp.net core的開源Web應用程式
框架
", "UiFramework": "UI框架", - "EmailAddress": "電子信箱地址" + "EmailAddress": "電子信箱地址", + "SelectLanguage": "选择语言" } } \ No newline at end of file diff --git a/common.DotSettings b/common.DotSettings index 6f40d029a7..5c1cda48fd 100644 --- a/common.DotSettings +++ b/common.DotSettings @@ -20,6 +20,18 @@ False False SQL + False + Never + Never + False + Never + Never + Never + Never + Never + True + True + True False False False diff --git a/common.props b/common.props index 9f6b677e72..5254c91876 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ latest - 4.0.0 + 4.3.0 $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ @@ -9,8 +9,14 @@ git https://github.com/abpframework/abp/ true + + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - + + all + runtime; build; native; contentfiles; analyzers + - \ No newline at end of file + diff --git a/docs/en/API/Dynamic-CSharp-API-Clients.md b/docs/en/API/Dynamic-CSharp-API-Clients.md index 23ec336acf..b1d71e0bb1 100644 --- a/docs/en/API/Dynamic-CSharp-API-Clients.md +++ b/docs/en/API/Dynamic-CSharp-API-Clients.md @@ -1,10 +1,23 @@ -# Dynamic C# API Clients +# Dynamic C# API Client Proxies -ABP can dynamically create C# API client proxies to call remote HTTP services (REST APIs). In this way, you don't need to deal with `HttpClient` and other low level HTTP features to call remote services and get results. +ABP can dynamically create C# API client proxies to call your remote HTTP services (REST APIs). In this way, you don't need to deal with `HttpClient` and other low level details to call remote services and get results. + +Dynamic C# proxies automatically handles the following stuff for you; + +* Maps C# **method calls** to remote server **HTTP calls** by considering the HTTP method, route, query string parameters, request payload and other details. +* **Authenticates** the HTTP Client by adding access token to the HTTP header. +* **Serializes** to and deserialize from JSON. +* Handles HTTP API **versioning**. +* Add **correlation id**, current **tenant** id and the current **culture** to the request. +* Properly **handles the error messages** sent by the server and throws proper exceptions. + +This system can be used by any type of .NET client to consume your HTTP APIs. ## Service Interface -Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project. Example: +Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project, typically in the `Application.Contracts` project if you've created your solution using the startup templates. + +Example: ````csharp public interface IBookAppService : IApplicationService @@ -13,7 +26,7 @@ public interface IBookAppService : IApplicationService } ```` -Your interface should implement the `IRemoteService` interface to be automatically discovered. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookAppService` above satisfies this condition. +> Your interface should implement the `IRemoteService` interface to be automatically discovered. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookAppService` above satisfies this condition. Implement this class in your service application. You can use [auto API controller system](Auto-API-Controllers.md) to expose the service as a REST API endpoint. @@ -55,6 +68,8 @@ public class MyClientAppModule : AbpModule `AddHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, creates and registers proxy classes. +> The startup templates already comes pre-configured for the client proxy generation, in the `HttpApi.Client` project. + ### Endpoint Configuration `RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. Simplest configuration is shown below: @@ -106,7 +121,7 @@ While you can inject `IBookAppService` like above to use the client proxy, you c ### AbpRemoteServiceOptions -`AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can use `Configure` method to set or override it. Example: +`AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can configure it in the `ConfigureServices` method of your [module](../Module-Development-Basics.md) to set or override it. Example: ````csharp public override void ConfigureServices(ServiceConfigurationContext context) @@ -162,4 +177,30 @@ context.Services.AddHttpClientProxies( Using `asDefaultServices: false` may only be needed if your application has already an implementation of the service and you do not want to override/replace the other implementation by your client proxy. -> If you disable `asDefaultServices`, you can only use `IHttpClientProxy` interface to use the client proxies. See the *IHttpClientProxy Interface* section above. \ No newline at end of file +> If you disable `asDefaultServices`, you can only use `IHttpClientProxy` interface to use the client proxies. See the *IHttpClientProxy Interface* section above. + +### Retry/Failure Logic & Polly Integration + +If you want to add retry logic for the failing remote HTTP calls for the client proxies, you can configure the `AbpHttpClientBuilderOptions` in the `PreConfigureServices` method of your module class. + +**Example: Use the [Polly](https://github.com/App-vNext/Polly) library to re-try 3 times on a failure** + +````csharp +public override void PreConfigureServices(ServiceConfigurationContext context) +{ + PreConfigure(options => + { + options.ProxyClientBuildActions.Add((remoteServiceName, clientBuilder) => + { + clientBuilder.AddTransientHttpErrorPolicy(policyBuilder => + policyBuilder.WaitAndRetryAsync( + 3, + i => TimeSpan.FromSeconds(Math.Pow(2, i)) + ) + ); + }); + }); +} +```` + +This example uses the [Microsoft.Extensions.Http.Polly](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly) package. You also need to import the `Polly` namespace (`using Polly;`) to be able to use the `WaitAndRetryAsync` method. \ No newline at end of file diff --git a/docs/en/Application-Services.md b/docs/en/Application-Services.md index f48ff9cb5b..7fe9825f2a 100644 --- a/docs/en/Application-Services.md +++ b/docs/en/Application-Services.md @@ -342,12 +342,12 @@ public class DistrictAppService { } - protected override async Task DeleteByIdAsync(DistrictKey id) + protected async override Task DeleteByIdAsync(DistrictKey id) { await Repository.DeleteAsync(d => d.CityId == id.CityId && d.Name == id.Name); } - protected override async Task GetEntityByIdAsync(DistrictKey id) + protected async override Task GetEntityByIdAsync(DistrictKey id) { return await AsyncQueryableExecuter.FirstOrDefaultAsync( Repository.Where(d => d.CityId == id.CityId && d.Name == id.Name) @@ -400,7 +400,7 @@ public class MyPeopleAppService : CrudAppService { } - protected override async Task CheckDeletePolicyAsync() + protected async override Task CheckDeletePolicyAsync() { await AuthorizationService.CheckAsync("..."); } diff --git a/docs/en/AspNet-Boilerplate-Migration-Guide.md b/docs/en/AspNet-Boilerplate-Migration-Guide.md index c7cc8a9e53..f4c2ec712b 100644 --- a/docs/en/AspNet-Boilerplate-Migration-Guide.md +++ b/docs/en/AspNet-Boilerplate-Migration-Guide.md @@ -438,7 +438,7 @@ ABP Framework uses and extends ASP.NET Core's [distributed caching abstraction]( ### Logging -ASP.NET Boilerplate uses Castle Windsor's [logging facility](http://docs.castleproject.org/Windsor.Logging-Facility.ashx) as an abstraction and supports multiple logging providers including Log4Net (the default one comes with the startup projects) and Serilog. You typically property-inject the logger: +ASP.NET Boilerplate uses Castle Windsor's [logging facility](https://github.com/castleproject/Windsor/blob/master/docs/logging-facility.md) as an abstraction and supports multiple logging providers including Log4Net (the default one comes with the startup projects) and Serilog. You typically property-inject the logger: ````csharp using Castle.Core.Logging; //1: Import Logging namespace diff --git a/docs/en/Authentication/Social-External-Logins.md b/docs/en/Authentication/Social-External-Logins.md index 309d4977a5..e80e53d4bd 100644 --- a/docs/en/Authentication/Social-External-Logins.md +++ b/docs/en/Authentication/Social-External-Logins.md @@ -1,32 +1,3 @@ # Social/External Logins -The [Account Module](../Modules/Account.md) has already configured to handle social or external logins out of the box. You can follow the ASP.NET Core documentation to add a social/external login provider to your application. - -## Example: Facebook Authentication - -Follow the [ASP.NET Core Facebook integration document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to support the Facebook login for your application. - -#### Add the NuGet Package - -Add the [Microsoft.AspNetCore.Authentication.Facebook](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Facebook) package to your project. Based on your architecture, this can be `.Web`, `.IdentityServer` (for tiered setup) or `.Host` project. - -#### Configure the Provider - -Use the `.AddFacebook(...)` extension method in the `ConfigureServices` method of your [module](../Module-Development-Basics.md), to configure the client: - -````csharp -context.Services.AddAuthentication() - .AddFacebook(facebook => - { - facebook.AppId = "..."; - facebook.AppSecret = "..."; - facebook.Scope.Add("email"); - facebook.Scope.Add("public_profile"); - }); -```` - -> It would be a better practice to use the `appsettings.json` or the ASP.NET Core User Secrets system to store your credentials, instead of a hard-coded value like that. Follow the [Microsoft's document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to learn the user secrets usage. - -## Angular UI - -Beginning from the v3.1, the Angular UI uses authorization code flow (as a best practice) to authenticate the user by redirecting to the MVC UI login page. So, even if you are using the Angular UI, social/external login integration is same as explained above and it will work out of the box. \ No newline at end of file +> This document has been moved. See the [Account Module](../Modules/Account.md) documentation. \ No newline at end of file diff --git a/docs/en/Authorization.md b/docs/en/Authorization.md index eef4237039..fbb71bb3d4 100644 --- a/docs/en/Authorization.md +++ b/docs/en/Authorization.md @@ -234,7 +234,7 @@ context When you write this code inside your permission definition provider, it finds the "role deletion" permission of the [Identity Module](Modules/Identity.md) and disabled the permission, so no one can delete a role on the application. -> Tip: It is better to check the value returned by the `GetPermissionOrNull` method since it may return null if the given permission was not defined. +> Tip: It is better to check the value returned by the `GetPermissionOrNull` method since it may return null if the given permission was not defined. ## IAuthorizationService @@ -280,21 +280,11 @@ public async Task CreateAsync(CreateAuthorDto input) ## Check a Permission in JavaScript -You may need to check a policy/permission on the client side. +See the following documents to learn how to re-use the authorization system on the client side: -### MVC UI - -For ASP.NET Core MVC / Razor Pages applications, you can use the `abp.auth` API. - -**Example: Check if a given permission has been granted for the current user** - -```js -abp.auth.isGranted('MyPermissionName'); -``` - -### Angular UI - -See the [permission management document](UI/Angular/Permission-Management.md) for the Angular UI. +* [ASP.NET Core MVC / Razor Pages UI: Authorization](UI/AspNetCore/JavaScript-API/Auth.md) +* [Angular UI Authorization](UI/Angular/Permission-Management.md) +* [Blazor UI Authorization](UI/Blazor/Authorization.md) ## Permission Management @@ -354,7 +344,7 @@ public class SystemAdminPermissionValueProvider : PermissionValueProvider public override string Name => "SystemAdmin"; - public override async Task + public async override Task CheckAsync(PermissionValueCheckContext context) { if (context.Principal?.FindFirst("User_Type")?.Value == "SystemAdmin") @@ -406,5 +396,5 @@ This is already done for the startup template integration tests. ## See Also * [Permission Management Module](Modules/Permission-Management.md) -* [ASP.NET Core MVC / Razor Pages JavaScript Auth API](API/JavaScript-API/Auth.md) +* [ASP.NET Core MVC / Razor Pages JavaScript Auth API](UI/AspNetCore/JavaScript-API/Auth.md) * [Permission Management in Angular UI](UI/Angular/Permission-Management.md) diff --git a/docs/en/Background-Jobs-RabbitMq.md b/docs/en/Background-Jobs-RabbitMq.md index a984a2a3ab..2ee478ed17 100644 --- a/docs/en/Background-Jobs-RabbitMq.md +++ b/docs/en/Background-Jobs-RabbitMq.md @@ -1,3 +1,133 @@ # RabbitMQ Background Job Manager -TODO \ No newline at end of file +RabbitMQ is an industry standard message broker. While it is typically used for inter-process communication (messaging / distributed events), it is pretty useful to store and execute background jobs in FIFO (First In First Out) order. + +ABP Framework provides the [Volo.Abp.BackgroundJobs.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.RabbitMQ) NuGet package to use the RabbitMQ for background job execution. + +> See the [background jobs document](Background-Jobs.md) to learn how to use the background job system. This document only shows how to install and configure the RabbitMQ integration. + +## Installation + +Use the ABP CLI to add [Volo.Abp.BackgroundJobs.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.RabbitMQ) NuGet package to your project: + +* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. +* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BackgroundJobs.RabbitMQ` package. +* Run `abp add-package Volo.Abp.BackgroundJobs.RabbitMQ` command. + +If you want to do it manually, install the [Volo.Abp.BackgroundJobs.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.BackgroundJobs.RabbitMQ) NuGet package to your project and add `[DependsOn(typeof(AbpBackgroundJobsRabbitMqModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. + +## Configuration + +### Default Configuration + +The default configuration automatically connects to the local RabbitMQ server (localhost) with the standard port. **In this case, no configuration needed.** + +### RabbitMQ Connection(s) + +You can configure the RabbitMQ connections using the standard [configuration system](Configuration.md), like using the `appsettings.json` file, or using the [options](Options.md) classes. + +#### `appsettings.json` file configuration + +This is the simplest way to configure the RabbitMQ connections. It is also very strong since you can use any other configuration source (like environment variables) that is [supported by the AspNet Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/). + +**Example: Configuring the Default RabbitMQ Connection** + +````json +{ + "RabbitMQ": { + "Connections": { + "Default": { + "HostName": "123.123.123.123", + "Port": "5672" + } + } + } +} +```` + +You can use any of the [ConnectionFactry](http://rabbitmq.github.io/rabbitmq-dotnet-client/api/RabbitMQ.Client.ConnectionFactory.html#properties) properties as the connection properties. See [the RabbitMQ document](https://www.rabbitmq.com/dotnet-api-guide.html#exchanges-and-queues) to understand these options better. + +Defining multiple connections is allowed. In this case, you can use different connections for different background job types (see the `AbpRabbitMqBackgroundJobOptions` section below). + +**Example: Declare two connections** + +````json +{ + "RabbitMQ": { + "Connections": { + "Default": { + "HostName": "123.123.123.123" + }, + "SecondConnection": { + "HostName": "321.321.321.321" + } + } + } +} +```` + +#### AbpRabbitMqOptions + +`AbpRabbitMqOptions` class can be used to configure the connection strings for the RabbitMQ. You can configure this options inside the `ConfigureServices` of your [module](Module-Development-Basics.md). + +**Example: Configure the connection** + +````csharp +Configure(options => +{ + options.Connections.Default.UserName = "user"; + options.Connections.Default.Password = "pass"; + options.Connections.Default.HostName = "123.123.123.123"; + options.Connections.Default.Port = 5672; +}); +```` + +Using these options classes can be combined with the `appsettings.json` way. Configuring an option property in the code overrides the value in the configuration file. + +### AbpRabbitMqBackgroundJobOptions + +#### Job Queue Names + +By default, each job type uses a separate queue. Queue names are calculated by combining a standard prefix and the job name. Default prefix is `AbpBackgroundJobs.` So, if the job name is `EmailSending` then the queue name in the RabbitMQ becomes `AbpBackgroundJobs.EmailSending` + +> Use `BackgroundJobName` attribute on the background **job argument** class to specify the job name. Otherwise, the job name will be the full name (with namespace) of the job class. + +#### Job Connections + +By default, all the job types use the `Default` RabbitMQ connection. + +#### Customization + +`AbpRabbitMqBackgroundJobOptions` can be used to customize the queue names and the connections used by the jobs. + +**Example:** + +````csharp +Configure(options => +{ + options.DefaultQueueNamePrefix = "my_app_jobs."; + options.JobQueues[typeof(EmailSendingArgs)] = + new JobQueueConfiguration( + typeof(EmailSendingArgs), + queueName: "my_app_jobs.emails", + connectionName: "SecondConnection" + ); +}); +```` + +* This example sets the default queue name prefix to `my_app_jobs.`. If different applications use the same RabbitMQ server, it would be important to use different prefixes for each application to not consume jobs of each other. +* Also specifies a different connection string for the `EmailSendingArgs`. + +`JobQueueConfiguration` class has some additional options in its constructor; + +* `queueName`: The queue name that is used for this job. The prefix is not added, so you need to specify the full name of the queue. +* `connectionName`: The RabbitMQ connection name (see the connection configuration above). This is optional and the default value is `Default`. +* `durable` (optional, default: `true`). +* `exclusive` (optional, default: `false`). +* `autoDelete` (optional, default: `false`) + +See the RabbitMQ documentation if you want to understand the `durable`, `exclusive` and `autoDelete` options better, while most of the times the default configuration is what you want. + +## See Also + +* [Background Jobs](Background-Jobs.md) \ No newline at end of file diff --git a/docs/en/Background-Jobs.md b/docs/en/Background-Jobs.md index e5ef87bb2d..2e0b5d3f65 100644 --- a/docs/en/Background-Jobs.md +++ b/docs/en/Background-Jobs.md @@ -13,7 +13,7 @@ Background jobs are **persistent** that means they will be **re-tried** and **ex ABP provides an **abstraction** module and **several implementations** for background jobs. It has a built-in/default implementation as well as Hangfire, RabbitMQ and Quartz integrations. -`Volo.Abp.BackgroundJobs.Abstractions` nuget package provides needed services to create background jobs and queue background job items. If your module only depend on this package, it can be independent from the actual implementation/integration. +`Volo.Abp.BackgroundJobs.Abstractions` NuGet package provides needed services to create background jobs and queue background job items. If your module only depend on this package, it can be independent from the actual implementation/integration. > `Volo.Abp.BackgroundJobs.Abstractions` package is installed to the startup templates by default. @@ -24,23 +24,29 @@ A background job is a class that implements the `IBackgroundJob` interfac This example is used to send emails in background. First, define a class to store arguments of the background job: ````csharp -public class EmailSendingArgs +namespace MyProject { - public string EmailAddress { get; set; } - public string Subject { get; set; } - public string Body { get; set; } + public class EmailSendingArgs + { + public string EmailAddress { get; set; } + public string Subject { get; set; } + public string Body { get; set; } + } } ```` Then create a background job class that uses an `EmailSendingArgs` object to send an email: ````csharp +using System.Threading.Tasks; using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; using Volo.Abp.Emailing; namespace MyProject { - public class EmailSendingJob : BackgroundJob, ITransientDependency + public class EmailSendingJob + : AsyncBackgroundJob, ITransientDependency { private readonly IEmailSender _emailSender; @@ -49,9 +55,9 @@ namespace MyProject _emailSender = emailSender; } - public override void Execute(EmailSendingArgs args) + public override async Task ExecuteAsync(EmailSendingArgs args) { - _emailSender.Send( + await _emailSender.SendAsync( args.EmailAddress, args.Subject, args.Body @@ -63,10 +69,35 @@ namespace MyProject This job simply uses `IEmailSender` to send emails (see [email sending document](Emailing.md)). +> `AsyncBackgroundJob` is used to create a job needs to perform async calls. You can inherit from `BackgroundJob` and override the `Execute` method if the method doesn't need to perform any async call. + #### Exception Handling A background job should not hide exceptions. If it throws an exception, the background job is automatically re-tried after a calculated waiting time. Hide exceptions only if you don't want to re-run the background job for the current argument. +#### Job Name + +Each background job has a name. Job names are used in several places. For example, RabbitMQ provider uses job names to determine the RabbitMQ Queue names. + +Job name is determined by the **job argument type**. For the `EmailSendingArgs` example above, the job name is `MyProject.EmailSendingArgs` (full name, including the namespace). You can use the `BackgroundJobName` attribute to set a different job name. + +**Example** + +```csharp +using Volo.Abp.BackgroundJobs; + +namespace MyProject +{ + [BackgroundJobName("emails")] + public class EmailSendingArgs + { + public string EmailAddress { get; set; } + public string Subject { get; set; } + public string Body { get; set; } + } +} +``` + ### Queue a Job Item Now, you can queue an email sending job using the `IBackgroundJobManager` service: diff --git a/docs/en/Background-Workers-Quartz.md b/docs/en/Background-Workers-Quartz.md index e1d8287019..ce82222c14 100644 --- a/docs/en/Background-Workers-Quartz.md +++ b/docs/en/Background-Workers-Quartz.md @@ -37,7 +37,7 @@ public class YourModule : AbpModule ```` > Quartz background worker integration provided `QuartzPeriodicBackgroundWorkerAdapter` to adapt `PeriodicBackgroundWorkerBase` and `AsyncPeriodicBackgroundWorkerBase` derived class. So, you can still fllow the [background workers document](Background-Workers.md) to define the background worker. -> `BackgroundJobWorker` checks todo jobs every 5 seconds, but quartz will not block when long time jobs are executing. So,after Added Quartz background worker integration, you also need to add [Quartz Background Jobs](Background-Jobs-Quartz.md) or [Hangfire Background Jobs](Background-Jobs-Hangfire.md) to avoid duplicate execution jobs. +> `BackgroundJobWorker` checks jobs every 5 seconds, but quartz will not block when long time jobs are executing. So,after Added Quartz background worker integration, you also need to add [Quartz Background Jobs](Background-Jobs-Quartz.md) or [Hangfire Background Jobs](Background-Jobs-Hangfire.md) to avoid duplicate execution jobs. ## Configuration diff --git a/docs/en/Background-Workers.md b/docs/en/Background-Workers.md index c8a617efec..b12f1881fb 100644 --- a/docs/en/Background-Workers.md +++ b/docs/en/Background-Workers.md @@ -45,7 +45,7 @@ Assume that we want to make a user passive, if the user has not logged in to the public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase { public PassiveUserCheckerWorker( - AbpTimer timer, + AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory ) : base( timer, @@ -54,7 +54,7 @@ public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase Timer.Period = 600000; //10 minutes } - protected override async Task DoWorkAsync( + protected async override Task DoWorkAsync( PeriodicBackgroundWorkerContext workerContext) { Logger.LogInformation("Starting: Setting status of inactive users..."); @@ -80,7 +80,7 @@ public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase ## Register Background Worker -After creating a background worker class, you should to add it to the `IBackgroundWorkerManager`. The most common place is the `OnApplicationInitialization` method of your module class: +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: ````csharp [DependsOn(typeof(AbpBackgroundWorkersModule))] @@ -137,4 +137,4 @@ ABP Framework's background worker system is good to implement periodic tasks. Ho ## See Also * [Quartz Integration for the background workers](Background-Workers-Quartz.md) -* [Background Jobs](Background-Jobs.md) \ No newline at end of file +* [Background Jobs](Background-Jobs.md) diff --git a/docs/en/Blob-Storing-Aliyun.md b/docs/en/Blob-Storing-Aliyun.md index 62e57e58c7..6cdebb3acc 100644 --- a/docs/en/Blob-Storing-Aliyun.md +++ b/docs/en/Blob-Storing-Aliyun.md @@ -55,7 +55,7 @@ Configure(options => * **RoleSessionName** ([NotNull]string): Used to identify the temporary access credentials, it is recommended to use different application users to distinguish. * **Policy** (string): Additional permission restrictions. See the [document](https://help.aliyun.com/document_detail/100680.html) for details. * **DurationSeconds** (int): Validity period(s) of a temporary access certificate,minimum is 900 and the maximum is 3600. -* **ContainerName** (string): You can specify the container name in Aliyun. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aliyun has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://help.aliyun.com/knowledge_detail/39668.html): +* **ContainerName** (string): You can specify the container name in Aliyun. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aliyun has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://help.aliyun.com/knowledge_detail/39668.html): * Container names must start or end with a letter or number, and can contain only letters, numbers, and the dash (-) character. * Container names Must start and end with lowercase letters and numbers. * Container names must be from **3** through **63** characters long. diff --git a/docs/en/Blob-Storing-Aws.md b/docs/en/Blob-Storing-Aws.md index cfee5c3f57..05e3caa183 100644 --- a/docs/en/Blob-Storing-Aws.md +++ b/docs/en/Blob-Storing-Aws.md @@ -60,7 +60,7 @@ Configure(options => * **Region** (string): The system name of the service. * **Policy** (string): An IAM policy in JSON format that you want to use as an inline session policy. * **DurationSeconds** (int): Validity period(s) of a temporary access certificate,minimum is 900 and the maximum is 3600. **note**: Using subaccounts operated OSS,if the value is 0. -* **ContainerName** (string): You can specify the container name in Aws. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aws has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): +* **ContainerName** (string): You can specify the container name in Aws. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aws has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): * Bucket names must be between **3** and **63** characters long. * Bucket names can consist only of **lowercase** letters, numbers, dots (.), and hyphens (-). * Bucket names must begin and end with a letter or number. diff --git a/docs/en/Blob-Storing-Azure.md b/docs/en/Blob-Storing-Azure.md index 9d033013da..de6ed63152 100644 --- a/docs/en/Blob-Storing-Azure.md +++ b/docs/en/Blob-Storing-Azure.md @@ -40,7 +40,7 @@ Configure(options => ### Options * **ConnectionString** (string): A connection string includes the authorization information required for your application to access data in an Azure Storage account at runtime using Shared Key authorization. Please refer to Azure documentation: https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string -* **ContainerName** (string): You can specify the container name in azure. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Azure has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names): +* **ContainerName** (string): You can specify the container name in azure. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Azure has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names): * Container names must start or end with a letter or number, and can contain only letters, numbers, and the dash (-) character. * Every dash (-) character must be immediately preceded and followed by a letter or number; consecutive dashes are not permitted in container names. * All letters in a container name must be **lowercase**. diff --git a/docs/en/Blob-Storing-Minio.md b/docs/en/Blob-Storing-Minio.md index 505c6721b0..6e0d7626b7 100644 --- a/docs/en/Blob-Storing-Minio.md +++ b/docs/en/Blob-Storing-Minio.md @@ -43,7 +43,7 @@ Configure(options => * **EndPoint** (string): URL to object storage service. Please refer to MinIO Client SDK for .NET: https://docs.min.io/docs/dotnet-client-quickstart-guide.html * **AccessKey** (string): Access key is the user ID that uniquely identifies your account. * **SecretKey** (string): Secret key is the password to your account. -* **BucketName** (string): You can specify the bucket name in MinIO. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)).MinIO is the defacto standard for S3 compatibility, So MinIO has some **rules for naming bucket**. The [following rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html) apply for naming MinIO buckets: +* **BucketName** (string): You can specify the bucket name in MinIO. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)).MinIO is the defacto standard for S3 compatibility, So MinIO has some **rules for naming bucket**. The [following rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html) apply for naming MinIO buckets: * Bucket names must be between **3** and **63** characters long. * Bucket names can consist only of **lowercase** letters, numbers, dots (.), and hyphens (-). * Bucket names must begin and end with a letter or number. diff --git a/docs/en/Blob-Storing.md b/docs/en/Blob-Storing.md index f477e6178a..82c2a861ab 100644 --- a/docs/en/Blob-Storing.md +++ b/docs/en/Blob-Storing.md @@ -14,14 +14,14 @@ ABP BLOB Storage system is also compatible to other ABP Framework features like ## BLOB Storage Providers -The ABP Framework has already the following storage provider implementations; +The ABP Framework has already the following storage provider implementations: * [File System](Blob-Storing-File-System.md): Stores BLOBs in a folder of the local file system, as standard files. * [Database](Blob-Storing-Database.md): Stores BLOBs in a database. * [Azure](Blob-Storing-Azure.md): Stores BLOBs on the [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/). * [Aliyun](Blob-Storing-Aliyun.md): Stores BLOBs on the [Aliyun Storage Service](https://help.aliyun.com/product/31815.html). * [Minio](Blob-Storing-Minio.md): Stores BLOBs on the [MinIO Object storage](https://min.io/). -* [Aws](Blob-Storing-Aws.md): Stores BLOBs on the[Amazon Simple Storage Service](https://min.io/). +* [Aws](Blob-Storing-Aws.md): Stores BLOBs on the [Amazon Simple Storage Service](https://aws.amazon.com/s3/). More providers will be implemented by the time. You can [request](https://github.com/abpframework/abp/issues/new) it for your favorite provider or [create it yourself](Blob-Storing-Custom-Provider.md) and [contribute](Contribution/Index.md) to the ABP Framework. diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/Post.md b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/Post.md index b6c1d3f952..c09b0eb75c 100644 --- a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/Post.md +++ b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/Post.md @@ -16,7 +16,7 @@ ABP.IO Platform is rapidly growing and we are getting more and more contribution ### Object Extending System -In the last few releases, we've mostly focused on providing ways to extend existing modules when you use them as NuGet/NPM Packages. +In the last few releases, we've mostly focused on providing ways to extend existing modules when you use them as NuGet/NPM Packages. The Object Extending System allows module developers to create extensible modules and allows application developers to customize and extend a module easily. @@ -43,7 +43,7 @@ ObjectExtensionManager.Instance options.Attributes.Add(new RequiredAttribute()); options.Attributes.Add( new StringLengthAttribute(32) { - MinimumLength = 6 + MinimumLength = 6 } ); }); @@ -121,7 +121,7 @@ Just create a class derived from the `ExceptionSubscriber` class in your applica ````csharp public class MyExceptionSubscriber : ExceptionSubscriber { - public override async Task HandleAsync(ExceptionNotificationContext context) + public async override Task HandleAsync(ExceptionNotificationContext context) { //TODO... } @@ -244,4 +244,4 @@ We ([Volosoft](https://volosoft.com/) - the core team behind the ABP.IO platform [ABP Framework](https://abp.io/) provides all the infrastructure and application independent framework features to make you more productive, focus on your own business code and implement software development best practices. It provides you a well defined and comfortable development experience without repeating yourself. -[ABP Commercial](https://commercial.abp.io/) provides pre-built functionalities, themes and tooling to save your time if your requirements involve these functionalities in addition to the premium support for the framework and the pre-built modules. \ No newline at end of file +[ABP Commercial](https://commercial.abp.io/) provides pre-built functionalities, themes and tooling to save your time if your requirements involve these functionalities in addition to the premium support for the framework and the pre-built modules. diff --git a/docs/en/Blog-Posts/2020-11-12 v4_0_Preview/POST.md b/docs/en/Blog-Posts/2020-11-12 v4_0_Preview/POST.md new file mode 100644 index 0000000000..03e3d31f97 --- /dev/null +++ b/docs/en/Blog-Posts/2020-11-12 v4_0_Preview/POST.md @@ -0,0 +1,143 @@ +# ABP Framework 4.0 RC Has Been Published based on .NET 5.0! + +Today, we have released the [ABP Framework](https://abp.io/) (and the [ABP Commercial](https://commercial.abp.io/)) 4.0.0 RC that is based on the **.NET 5.0**. This blog post introduces the new features and important changes in the new version. + +> **The planned release date for the [4.0.0 final](https://github.com/abpframework/abp/milestone/45) version is November 26, 2020**. + +## Get Started with the 4.0 RC + +If you want to try the version `4.0.0` today, follow the steps below; + +1) **Upgrade** the ABP CLI to the version `4.0.0-rc.3` using a command line terminal: + +````bash +dotnet tool update Volo.Abp.Cli -g --version 4.0.0-rc.3 +```` + +**or install** if you haven't installed before: + +````bash +dotnet tool install Volo.Abp.Cli -g --version 4.0.0-rc.3 +```` + +2) Create a **new application** with the `--preview` option: + +````bash +abp new BookStore --preview +```` + +See the [ABP CLI documentation](https://docs.abp.io/en/abp/3.3/CLI) for all the available options. + +> You can also use the *Direct Download* tab on the [Get Started](https://abp.io/get-started) page by selecting the **Preview checkbox**. + +## Migrating From 3.x to 4.0 + +The version 4.0 comes with some major changes including the **migration from .NET Core 3.1 to .NET 5.0**. + +We've prepared a **detailed [migration document](https://docs.abp.io/en/abp/4.0/Migration-Guides/Abp-4_0)** to explain all the changes and the actions you need to take while upgrading your existing solutions. + +## What's new with the ABP Framework 4.0 + +### The Blazor UI + +The Blazor UI is now stable and officially supported. The [web application development tutorial](https://docs.abp.io/en/abp/4.0/Tutorials/Part-1?UI=Blazor) has been updated based on the version 4.0. + +#### abp bundle command + +Introducing the `abp bundle` CLI command to manage static JavaScript & CSS file dependencies of a Blazor application. This command is currently used to add the dependencies to the `index.html` file in the dependency order by respecting to modularity. In the next version it will automatically unify & minify the files. The documentation is being prepared. + +#### Removed the JQuery & Bootstrap JavaScript + +Removed JQuery & Bootstrap JavaScript dependencies for the Blazor UI. + +>There are some other changes in the startup template and some public APIs. Follow the [Migration Guide](https://docs.abp.io/en/abp/4.0/Migration-Guides/Abp-4_0) to apply changes for existing solutions that you're upgrading from the version 3.3. While we will continue to make improvements add new features, we no longer make breaking changes on the existing APIs until the version 5.0. + +#### Others + +A lot of minor and major improvements have been done for the Blazor UI. Some of them are listed below: + +* Implemented `IComponentActivator` to resolve the component from the `IServiceProvider`. So, you can now inject dependencies into the constructor of your razor component. +* Introduced the `AbpComponentBase` base class that you derive your components from. It has useful base properties that you can use in your pages/components. +* Introduced `IUiNotificationService` service to show toast notifications on the UI. +* Improved the `IUiMessageService` to show message & confirmation dialogs. + +### System.Text.Json + +ABP Framework 4.0 uses the System.Text.Json by default as the JSON serialization library. It, actually, using a hybrid approach: Continues to use the Newtonsoft.Json when it needs to use the features not supported by the System.Text.Json. + +Follow the [Migration Guide](https://docs.abp.io/en/abp/4.0/Migration-Guides/Abp-4_0) to learn how to configure to use the Newtonsoft.Json for some specific types or switch back to the Newtonsoft.Json as the default JSON serializer. + +### Identity Server 4 Upgrade + +ABP Framework upgrades the [IdentityServer4](https://www.nuget.org/packages/IdentityServer4) library from 3.x to 4.1.1 with the ABP Framework version 4.0. IdentityServer 4.x has a lot of changes. Some of them are **breaking changes in the data structure**. + +Follow the [Migration Guide](https://docs.abp.io/en/abp/4.0/Migration-Guides/Abp-4_0) to upgrade existing solutions. + +### Creating a New Module Inside the Application + +ABP CLI has now a command to create a new module and add it to an existing solution. In this way, you can create modular applications easier than before. + +Example: Create a *ProductManagement* module into your solution. + +````bash +abp add-module ProductManagement --new --add-to-solution-file +```` + +Execute this command in a terminal in the root folder of your solution. If you don't specify the `--add-to-solution-file` option, then the module projects will not be added to the main solution, but the project references still be added. In this case, you need to open the module's solution to develop the module. + +See the [CLI document](https://docs.abp.io/en/abp/4.0/CLI) for other options. + +### WPF Startup Template + +Introducing the WPF startup template for the ABP Framework. Use the ABP CLI new command to create a new WPF application: + +````bash +abp new MyWpfApp -t wpf +```` + +This is a minimalist, empty project template that is integrated to the ABP Framework. + +### New Languages + +**Thanks to the contributors** from the ABP Community, the framework modules and the startup template have been localized to **German** language by [Alexander Pilhar](https://github.com/alexanderpilhar) & [Nico Lachmuth](https://github.com/tntwist). + +### Other Notes + +* Upgraded to Angular 11. +* Since [Mongo2Go](https://github.com/Mongo2Go/Mongo2Go) library not supports transactions, you can use transactions in unit tests for MongoDB. + +## What's new with the ABP Commercial 4.0 + +### The Blazor UI + +The Blazor UI for the ABP Commercial is also becomes stable and feature rich with the version 4.0; + +* [ABP Suite](https://commercial.abp.io/tools/suite) now supports to generate CRUD pages for the Blazor UI. +* Completed the [Lepton Theme](https://commercial.abp.io/themes) for the Blazor UI. +* Implemented the [File Management](https://commercial.abp.io/modules/Volo.FileManagement) module for the Blazor UI. + +### The ABP Suite + +While creating create/edit modals with a navigation property, we had two options: A dropdown to select the target entity and a modal to select the entity by searching with a data table. + +Dropdown option now supports **lazy load, search and auto-complete**. In this way, selecting a navigation property becomes much easier and supports large data sets on the dropdown. + +**Example: Select an author while creating a new book** + +![abp-suite-auto-complete-dropdown](abp-suite-auto-complete-dropdown.png) + +With the new version, you can **disable backend code generation** on CRUD page generation. This is especially useful if you want to regenerate the page with a different UI framework, but don't want to regenerate the server side code. + +### Identity Server Management UI Revised + +Completely revised the Identity Server Management UI based on the IDS 4.x changes. + +## About the Next Release + +The next feature version, `4.1.0`, will mostly focus on completing the missing documents, fixing bugs, performance optimizations and improving the Blazor UI features. The planned preview release date for the version `4.1.0` is December 10 and the final (stable) version release date is December 24. + +Follow the [GitHub milestones](https://github.com/abpframework/abp/milestones) for all the planned ABP Framework version release dates. + +## Feedback + +Please check out the ABP Framework 4.0.0 RC and [provide feedback](https://github.com/abpframework/abp/issues/new) to help us to release a more stable version. **The planned release date for the [4.0.0 final](https://github.com/abpframework/abp/milestone/45) version is November 26**. diff --git a/docs/en/Blog-Posts/2020-11-12 v4_0_Preview/abp-suite-auto-complete-dropdown.png b/docs/en/Blog-Posts/2020-11-12 v4_0_Preview/abp-suite-auto-complete-dropdown.png new file mode 100644 index 0000000000..2021ef42bd Binary files /dev/null and b/docs/en/Blog-Posts/2020-11-12 v4_0_Preview/abp-suite-auto-complete-dropdown.png differ diff --git a/docs/en/Blog-Posts/2020-12-04 v4_0_Release_Stable/POST.md b/docs/en/Blog-Posts/2020-12-04 v4_0_Release_Stable/POST.md new file mode 100644 index 0000000000..840b5585fd --- /dev/null +++ b/docs/en/Blog-Posts/2020-12-04 v4_0_Release_Stable/POST.md @@ -0,0 +1,87 @@ +# ABP.IO Platform 4.0 with .NET 5.0 in the 4th Year! + +Today, we are extremely happy to release ABP Framework 4.0 with **.NET 5.0 support**! + +## 4 Years of Work + +As a nice coincidence, today is the **4th year** since the first commit made in the [abp repository](https://github.com/abpframework/abp)! So, we can say "*Happy Birthday ABP Framework!*". + +![abp-contribution-graph-4-years](abp-contribution-graph-4-years.png) + +### Some Statistics + +ABP.IO Platform and the ABP Community is growing. Here, a summary of these 4 years. + +From GitHub, only from the main [abp repository](https://github.com/abpframework/abp); + +* **15,297 commits** done. +* **3,764 issues** are closed. +* **2,133 pull requests** are merged. +* **158 contributors**. +* **88 releases** published. +* **5.2K stars** on GitHub. + +From NuGet & NPM; + +* **220 NuGet** packages & **52 NPM** packages. +* **1,000,000 downloads** only for the core NuGet package. + +From Website; + +* **200,000 visitors**. +* **1,000,000+ sessions**. + +## What's New With 4.0? + +Since all the new features are already explained in details with the [4.0 RC Announcement Post](https://blog.abp.io/abp/ABP.IO-Platform-v4.0-RC-Has-Been-Released-based-on-.NET-5.0), I will not repeat all the details again. Please read [the RC post](https://blog.abp.io/abp/ABP.IO-Platform-v4.0-RC-Has-Been-Released-based-on-.NET-5.0) for **new feature and changes** you may need to do for your solution while upgrading to the version 4.0. + +Here, a brief list of major features and changes; + +* Migrated to **.NET 5.0**. +* Stable **Blazor** UI. +* Moved to **System.Text.Json**. +* Upgraded to **IdentityServer** version 4.0. +* **WPF** startup template. + +## Creating New Solutions + +You can create a new solution with the ABP Framework version 4.0 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started). + +> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for details. + +## How to Upgrade an Existing Solution + +This is a **major version** and requires some **manual work**, especially related to **.NET 5.0** and **IdentityServer** 4.0 upgrades. + +* See the [MIGRATION GUIDE](https://docs.abp.io/en/abp/latest/Migration-Guides/Abp-4_0) that covers all the details about the upgrade progress. + +* You can also see the [upgrading document](https://docs.abp.io/en/abp/latest/Upgrading). + +## New Guides / Documents + +We are constantly improving the documentation. Our purpose is not only document the ABP Framework, but also write architectural and practical guides for developers. + +### Implementing Domain Driven Design + +[Implementing Domain Driven Design](https://docs.abp.io/en/abp/latest/Domain-Driven-Design-Implementation-Guide) is a practical guide for they want to implement the DDD principles in their solutions. While the implementation details rely on the ABP Framework infrastructure, core concepts, principles and patterns are applicable in any kind of solution, even if it is not a .NET solution. + +![ddd-implementation-guide-sample](ddd-implementation-guide-sample.png) + +### Testing + +The new [Testing document](https://docs.abp.io/en/abp/latest/Testing) discusses different kind of automated tests and explains how you can write tests for your ABP based solutions. + +### UI Documents + +We've created a lot of documents for the [MVC](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Overall), [Blazor](https://docs.abp.io/en/abp/latest/UI/Blazor/Overall) and the [Angular](https://docs.abp.io/en/abp/latest/UI/Angular/Quick-Start) UI. + +## About the Next Version + +The next versions 4.1 will mostly focus on; + +* Improving current features. +* Complete module features for the Blazor UI. +* Improve developer experience and productivity. +* More documentation and examples. + +Planned preview date for the version **4.1 is December 17, 2020**. See the [Road Map](https://docs.abp.io/en/abp/latest/Road-Map) document and [GitHub Milestones](https://github.com/abpframework/abp/milestones) to learn what's planned for the next versions. We are trying to be clear about the coming features and the next release dates. \ No newline at end of file diff --git a/docs/en/Blog-Posts/2020-12-04 v4_0_Release_Stable/abp-contribution-graph-4-years.png b/docs/en/Blog-Posts/2020-12-04 v4_0_Release_Stable/abp-contribution-graph-4-years.png new file mode 100644 index 0000000000..cb778d8dfe Binary files /dev/null and b/docs/en/Blog-Posts/2020-12-04 v4_0_Release_Stable/abp-contribution-graph-4-years.png differ diff --git a/docs/en/Blog-Posts/2020-12-04 v4_0_Release_Stable/ddd-implementation-guide-sample.png b/docs/en/Blog-Posts/2020-12-04 v4_0_Release_Stable/ddd-implementation-guide-sample.png new file mode 100644 index 0000000000..3aab36e979 Binary files /dev/null and b/docs/en/Blog-Posts/2020-12-04 v4_0_Release_Stable/ddd-implementation-guide-sample.png differ diff --git a/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/POST.md b/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/POST.md new file mode 100644 index 0000000000..4154bf7241 --- /dev/null +++ b/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/POST.md @@ -0,0 +1,202 @@ +# ABP Framework 4.1 RC Has Been Published + +Today, we have released the [ABP Framework](https://abp.io/) (and the [ABP Commercial](https://commercial.abp.io/)) 4.1.0 RC. This blog post introduces the new features and important changes in this new version. + +> **The planned release date for the [4.1.0 final](https://github.com/abpframework/abp/milestone/47) version is January 4, 2021**. + +## Get Started with the 4.1 RC + +If you want to try the version `4.1.0` today, follow the steps below; + +1) **Upgrade** the ABP CLI to the version `4.1.0-rc.1` using a command line terminal: + +````bash +dotnet tool update Volo.Abp.Cli -g --version 4.1.0-rc.1 +```` + +**or install** if you haven't installed before: + +````bash +dotnet tool install Volo.Abp.Cli -g --version 4.1.0-rc.1 +```` + +2) Create a **new application** with the `--preview` option: + +````bash +abp new BookStore --preview +```` + +See the [ABP CLI documentation](https://docs.abp.io/en/abp/latest/CLI) for all the available options. + +> You can also use the *Direct Download* tab on the [Get Started](https://abp.io/get-started) page by selecting the **Preview checkbox**. + +## Breaking Changes + +This version has a minor breaking change if you'd injected a repository by class. This is not a problem for 99% of the applications. However, see [#6677](https://github.com/abpframework/abp/issues/6677) for the solution if that's a breaking change for you. + +## What's new with the ABP Framework 4.1 + +### Module Entity Extensions + +Module Entity Extension system provides a simple way of adding new properties to an existing entity defined by a module that is used by your application. This feature is now available also for the open source modules (identity and tenant-management). [The documentation](https://docs.abp.io/en/abp/latest/Module-Entity-Extensions) has been moved into the ABP Framework's documentation. + +**Example: Add "SocialSecurityNumber" property to the `IdentityUser` entity** + +````csharp +ObjectExtensionManager.Instance.Modules() + .ConfigureIdentity(identity => + { + identity.ConfigureUser(user => + { + user.AddOrUpdateProperty( //property type: string + "SocialSecurityNumber", //property name + property => + { + //validation rules + property.Attributes.Add(new RequiredAttribute()); + property.Attributes.Add( + new StringLengthAttribute(64) { + MinimumLength = 4 + } + ); + + //...other configurations for this property + } + ); + }); + }); +```` + +The new property becomes available on the UI, API and the database. You can even define navigation properties. This provides an easy way to extend existing modules while using them as NuGet packages. See [the document](https://docs.abp.io/en/abp/latest/Module-Entity-Extensions) for details. + +### Blazor UI Improvements + +Since the Blazor UI is relatively new in the ABP Framework, we continue to add features and make enhancements to fill the gap between other supported UI types. + +#### Bundling & Minification + +In the version 4.1, we had introduced the `abp bundle` command for the Blazor UI to add global script and style files of the depended modules into the `index.html`. It was a preparation for a real bundling & minification system. With the version 4.2, this command has been completed. + +Whenever you add a new module to your Blazor application, just type the `abp bundle` command in a command line terminal; + +* It finds all the global script/style files in your application and the modules your application directly or indirectly depends on, ordered by the module dependencies. +* Bundles all the scripts into a single file and minified the file (same for the styles). +* Add the single bundle file to the `index.html` file. + +Added a configuration into the `appsettings.json` file in the Blazor application in the application startup template to control the bundling mode: + +````js +{ + "AbpCli": { + "Bundle": { + "Mode": "BundleAndMinify" + } + } +} +```` + +Possible values are; + +* `BundleAndMinify`: Bundle all the files into a single file and minify the content. +* `Bundle`: Bundle all files into a single file, but not minify. +* `None`: Add files individually, do not bundle. + +See the [Global Scripts & Styles](https://docs.abp.io/en/abp/4.1/UI/Blazor/Global-Scripts-Styles) document for details. + +#### SubmitButton + +`SubmitButton` is a new component that simplifies to save a form: + +````html + +```` + +The main advantages of using this component instead of a standard `Button` with submit type is; It automatically blocks the submit button until the save operation has fully completed. This prevents multiple clicks by user. And it is shorter than doing all manually. See the [document](https://docs.abp.io/en/abp/4.1/UI/Blazor/SubmitButton). + +#### Other Blazor UI highlights + +* Implemented some **animations** (like opening/closing modals and dropdowns). +* Automatically **focus** to the first input when you open a modal form. + +Module extensibility system (mentioned above) for the Blazor UI is under development and not available yet. + +## What's new with the ABP Commercial 4.1 + +### Blazor UI Improvements + +We continue to complete missing modules and functionalities for the Blazor UI. + +#### Organization Unit Management + +Organization Management UI has been implemented for the Blazor UI. Example screenshot: + +![blazor-organization-units](blazor-organization-units.png) + +#### IdentityServer UI + +IdentityServer Management UI is also available for the Blazor UI now: + +![blazor-identityserver-ui](blazor-identityserver-ui.png) + +### Suite: Navigation Property Selection with Typeahead + +We had introduced auto-complete select style navigation property selection. With this release, it is fully supported by all the UI options. So, when you create an CRUD page with ABP Suite for entity that has 1 to Many relation to another entity, you can simply select the target entity with a typeahead style select component. Example screenshot: + +![type-ahead](type-ahead.png) + +### Spanish Language Translation + +We continue to add new language supports for the UI. In this version, translated the UI to **Spanish** language. + +![spanish-commercial-translation](spanish-commercial-translation.png) + +### Coming: Public Website with Integrated CMS Features + +In the next version, the application startup template will come with a public website application option. CMS Kit module will be installed in the website by default, that means newsletter, contact form, comments and some other new features will be directly usable in your applications. + +An early screenshot from the public website application home page: + +![abp-commercial-public-website](abp-commercial-public-website.png) + +## Other News + +### ABP Community Contents + +A lot of new contents have been published in the ABP Community Web Site in the last two weeks: + +* [How to Integrate the Telerik Blazor Components to the ABP Blazor UI](https://community.abp.io/articles/how-to-integrate-the-telerik-blazor-components-to-the-abp-blazor-ui-q8g31abb) by [EngincanV](https://github.com/EngincanV) +* [Using DevExpress Blazor UI Components With the ABP Framework](https://community.abp.io/articles/using-devexpress-blazor-ui-components-with-the-abp-framework-wrpoa8rw) by [@berkansasmaz](https://github.com/berkansasmaz) +* [Creating a new UI theme by copying the Basic Theme (for MVC UI)](https://community.abp.io/articles/creating-a-new-ui-theme-by-copying-the-basic-theme-for-mvc-ui-yt9b18io) by [@ebubekirdinc](https://github.com/ebubekirdinc) +* [Using Angular Material Components With the ABP Framework](https://community.abp.io/members/muhammedaltug) by [@muhammedaltug](https://github.com/muhammedaltug) +* [How to export Excel files from the ABP framework](https://community.abp.io/articles/how-to-export-excel-files-from-the-abp-framework-wm7nnw3n) by [bartvanhoey](https://github.com/bartvanhoey) +* [Creating an Event Organizer Application with the ABP Framework & Blazor UI](https://community.abp.io/articles/creating-an-event-organizer-application-with-the-blazor-ui-wbe0sf2z) by [@hikalkan](https://github.com/hikalkan) + +Thanks to all of the contributors. We are waiting for your contributions too. If you want to create content for the ABP Community, please visit [community.abp.io](https://community.abp.io/) website and submit your article. + +#### Be a Superhero on Day 1 with ABP.IO + +Thanks to [@lprichar](http://github.com/lprichar) prepared an awesome introduction video for the ABP.IO Platform: "[Be a Superhero on Day 1 with ABP.IO](https://www.youtube.com/watch?v=ea0Zx9DLcGA)". + +#### New Sample Application: Event Organizer + +This is a new example application developed using the ABP Framework and the Blazor UI. See [this article](https://community.abp.io/articles/creating-an-event-organizer-application-with-the-blazor-ui-wbe0sf2z) for a step by step implementation guide. + +![event-list-ui](event-list-ui.png) + +### Github Discussions + +We enabled the [GitHub Discussions for the abp repository](https://github.com/abpframework/abp/discussions) as another place to discuss ideas or get help for the ABP Framework. The ABP core team is spending time participating in discussions and answering to questions as much as possible. + +## About the Next Release(s) + +Beginning from the next version (4.2.0), we are starting to spend more effort on the **CMS Kit module**. The purpose of this module is to provide CMS primitives (e.g. **comments, tags, reactions, contents**...) and features (e.g. **blog, pages, surveys**) as pre-built and reusable components. Current blog module will be a part of the CMS Kit module. + +We will continue to prepare documents, guides, tutorials and examples. And surely, we will continue to make enhancements and optimizations on the current features. + +> The planned preview release date for the version 4.2.0 is January 14, 2021 and the final (stable) version release date is January 28, 2021. + +Follow the [GitHub milestones](https://github.com/abpframework/abp/milestones) for all the planned ABP Framework version release dates. + +## Feedback + +Please check out the ABP Framework 4.1.0 RC and [provide feedback](https://github.com/abpframework/abp/issues/new) to help us to release a more stable version. **The planned release date for the [4.1.0 final](https://github.com/abpframework/abp/milestone/45) version is January 4, 2021**. diff --git a/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/abp-commercial-public-website.png b/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/abp-commercial-public-website.png new file mode 100644 index 0000000000..8f01a9b385 Binary files /dev/null and b/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/abp-commercial-public-website.png differ diff --git a/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/blazor-identityserver-ui.png b/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/blazor-identityserver-ui.png new file mode 100644 index 0000000000..8bb53d5d6b Binary files /dev/null and b/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/blazor-identityserver-ui.png differ diff --git a/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/blazor-organization-units.png b/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/blazor-organization-units.png new file mode 100644 index 0000000000..5db13d61a2 Binary files /dev/null and b/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/blazor-organization-units.png differ diff --git a/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/event-list-ui.png b/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/event-list-ui.png new file mode 100644 index 0000000000..4f049c2339 Binary files /dev/null and b/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/event-list-ui.png differ diff --git a/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/spanish-commercial-translation.png b/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/spanish-commercial-translation.png new file mode 100644 index 0000000000..ab6ca06719 Binary files /dev/null and b/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/spanish-commercial-translation.png differ diff --git a/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/type-ahead.png b/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/type-ahead.png new file mode 100644 index 0000000000..7eabb748fd Binary files /dev/null and b/docs/en/Blog-Posts/2020-12-18 v4_1_Preview/type-ahead.png differ diff --git a/docs/en/Blog-Posts/2021-01-07 v4_1_Release_Stable/POST.md b/docs/en/Blog-Posts/2021-01-07 v4_1_Release_Stable/POST.md new file mode 100644 index 0000000000..926f1193d4 --- /dev/null +++ b/docs/en/Blog-Posts/2021-01-07 v4_1_Release_Stable/POST.md @@ -0,0 +1,51 @@ +# ABP.IO Platform 4.1 Final Has Been Released! + +[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 4.1 versions have been released today. + +## What's New With 4.1? + +Since all the new features are already explained in details with the [4.1 RC Announcement Post](https://blog.abp.io/abp/ABP.IO-Platform-v4.1-RC-Has-Been-Released), I will not repeat all the details again. See the [RC Blog Post](https://blog.abp.io/abp/ABP.IO-Platform-v4.1-RC-Has-Been-Released) for all the features and enhancements. + +## Creating New Solutions + +You can create a new solution with the ABP Framework version 4.1 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started). + +> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for details. + +## How to Upgrade an Existing Solution + +### Install/Update the ABP CLI + +First of all, install the ABP CLI or upgrade to the latest version. + +If you haven't installed yet: + +```bash +dotnet tool install -g Volo.Abp.Cli +``` + +To update an existing installation: + +```bash +dotnet tool update -g Volo.Abp.Cli +``` + +### ABP UPDATE Command + +[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides a handy command to update all the ABP related NuGet and NPM packages in your solution with a single command: + +```bash +abp update +``` + +Run this command in the root folder of your solution. + +## ABP Community + +We started to get more contributions by the community for the [ABP Community](https://community.abp.io/) contents. Thank you all! + +We will be adding **Video Content** sharing system in a short time. We are planning to create short video contents, especially to explore the new features in every release. Again, we will be waiting video contributions by the community :) + +## About the Next Versions + +Planned preview date for the version **4.2 is January 14, 2021**. See the [Road Map](https://docs.abp.io/en/abp/latest/Road-Map) document and [GitHub Milestones](https://github.com/abpframework/abp/milestones) to learn what's planned for the next versions. We are trying to be clear about the coming features and the next release dates. \ No newline at end of file diff --git a/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/POST.md b/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/POST.md new file mode 100644 index 0000000000..d16afb4244 --- /dev/null +++ b/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/POST.md @@ -0,0 +1,241 @@ +# ABP Framework 4.2 RC Has Been Published + +Today, we have released the [ABP Framework](https://abp.io/) and the [ABP Commercial](https://commercial.abp.io/) 4.2.0 RC (Release Candidate). This blog post introduces the new features and important changes in this new version. + +> **The planned release date for the [4.2.0 final](https://github.com/abpframework/abp/milestone/48) version is January 28, 2021**. + +## Get Started with the 4.2 RC + +If you want to try the version `4.2.0` today, follow the steps below; + +1) **Upgrade** the ABP CLI to the version `4.2.0-rc.2` using a command line terminal: + +````bash +dotnet tool update Volo.Abp.Cli -g --version 4.2.0-rc.2 +```` + +**or install** if you haven't installed before: + +````bash +dotnet tool install Volo.Abp.Cli -g --version 4.2.0-rc.2 +```` + +2) Create a **new application** with the `--preview` option: + +````bash +abp new BookStore --preview +```` + +See the [ABP CLI documentation](https://docs.abp.io/en/abp/latest/CLI) for all the available options. + +> You can also use the *Direct Download* tab on the [Get Started](https://abp.io/get-started) page by selecting the **Preview checkbox**. + +## What's new with the ABP Framework 4.2 + +## IRepository.GetQueryableAsync() + +> **This version comes with an important change about using `IQueryable` features over the [repositories](https://docs.abp.io/en/abp/4.2/Repositories). It is suggested to read this section carefully and apply in your applications.** + +`IRepository` interface inherits `IQueryable`, so you can directly use the standard LINQ extension methods, like `Where`, `OrderBy`, `First`, `Sum`... etc. + +**Example: Using LINQ directly over the repository object** + +````csharp +public class BookAppService : ApplicationService, IBookAppService +{ + private readonly IRepository _bookRepository; + + public BookAppService(IRepository bookRepository) + { + _bookRepository = bookRepository; + } + + public async Task DoItInOldWayAsync() + { + //Apply any standard LINQ extension method + var query = _bookRepository + .Where(x => x.Price > 10) + .OrderBy(x => x.Name); + + //Execute the query asynchronously + var books = await AsyncExecuter.ToListAsync(query); + } +} +```` + +*See [the documentation](https://docs.abp.io/en/abp/4.2/Repositories#iqueryable-async-operations) if you wonder what is the `AsyncExecuter`.* + +Beginning from the version 4.2, the recommended way is using `IRepository.GetQueryableAsync()` to obtain an `IQueryable`, then use the LINQ extension methods over it. + +**Example: Using the new GetQueryableAsync method** + +````csharp +public async Task DoItInNewWayAsync() +{ + //Use GetQueryableAsync to obtain the IQueryable first + var queryable = await _bookRepository.GetQueryableAsync(); + + //Then apply any standard LINQ extension method + var query = queryable + .Where(x => x.Price > 10) + .OrderBy(x => x.Name); + + //Finally, execute the query asynchronously + var books = await AsyncExecuter.ToListAsync(query); +} +```` + +ABP may start a database transaction when you get an `IQueryable` (If current [Unit Of Work](https://docs.abp.io/en/abp/latest/Unit-Of-Work) is transactional). In this new way, it is possible to **start the database transaction in an asynchronous way**. Previously, we could not get the advantage of asynchronous while starting the transactions. + +> **The new way has a significant performance and scalability gain. The old usage (directly using LINQ over the repositories) will be removed in the next major version.** You have a lot of time for the change, but we recommend to immediately take the action since the old usage has a big scalability problem. + +#### About IRepository Async Extension Methods + +Using IRepository Async Extension Methods has no such a problem. The examples below are pretty fine: + +````csharp +var countAll = await _personRepository + .CountAsync(); + +var count = await _personRepository + .CountAsync(x => x.Name.StartsWith("A")); + +var book1984 = await _bookRepository + .FirstOrDefaultAsync(x => x.Name == "John"); +```` + +See the [repository documentation](https://docs.abp.io/en/abp/4.2/Repositories#iqueryable-async-operations) to understand the relation between `IQueryable` and asynchronous operations. + +### Repository Bulk Operations + +This version adds the following methods to the repositories: + +* `InsertManyAsync` +* `UpdateManyAsync` +* `DeleteManyAsync` + +The purpose of these methods to insert, update or delete many entities in one call with a better performance. + +Currently, **MongoDB** provider implements these methods as a single bulk operation since MongoDB API natively supports. But current **Entity Framework Core** implementation is not a real bulk operation. Instead, it does its best with the native API of the EF Core. If you want to implement in a more performant way, you can [customize the bulk operations](https://docs.abp.io/en/abp/4.2/Entity-Framework-Core#customize-bulk-operations) with your own implementation or by using a library. We could find a good open source library for EF Core 5.0 to implement it. + +### Selecting DBMS on Template Creation + +[ABP CLI](https://docs.abp.io/en/abp/4.2/CLI#new) now has an option to specify the DBMS when you use EF Core as the database provider. + +**Example: Select MySQL as the DBMS** + +````bash +abp new BookStore -dbms mysql --preview +```` + +Available options: `SqlServer` (default), `MySQL`, `SQLite`, `Oracle-Devart`, `PostgreSQL`. See the [documentation](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Other-DBMS) to use any other DBMS or switch the DBMS later. + +One change related to this feature is that: Now, the startup template doesn't come with an **initial migration** file. This is because the database migrations are different based on your DBMS preference and should be re-created. However, when you first run the `.DbMigrator` application, it will create the initial migration and create the database just like before. + +> See The Initial Migration section in the [Getting Started](https://docs.abp.io/en/abp/4.2/Getting-Started-Running-Solution?DB=EF#database-migrations) document if you have problems on running the `.DbMigrator` application first time. + +### Swagger UI Login / Authorization + +Testing the swagger UI was requiring some additional work, especially your authentication server is separated from the application that hosts the Swagger UI. + +With the version 4.2, the startup templates come with the authorization pre-configured for you. An Authorize button is available when you open the Swagger UI: + +![swagger-authorize](swagger-authorize.png) + +When you click, it opens a modal to authorize: + +![swagger-authorize](swagger-authorize-modal.png) + +When you click to the Authorize button here, you are redirected to the login page to login with your username and password (default username is `admin` and password is `1q2w3E*`). + +> Remember to select the Scopes (typically **select all**) you want to use before clicking to the Authorize button. + +### Angular Unit Testing + +We've improved the modules and the startup template to setup and write unit tests easier with the Angular UI. See the [Angular Unit Testing document](https://docs.abp.io/en/abp/4.2/UI/Angular/Testing) for details. + +### Other News + +* Improved HTTP **request-response performance** by resolving dependencies in a deferred way in the action/page filters, interceptors and some other services. +* Removed `MultipleActiveResultSets` from connection strings for new templates for SQL Server, since the new EF Core gives a warning when using it. If you want to use it, you need to change the connection string yourself. +* Added `HardDeleteAsync` extension method that takes a predicate to delete multiple entities. This extension method is available if the entity [Soft Delete](https://docs.abp.io/en/abp/latest/Data-Filtering). +* Implemented the [Page Alerts](https://docs.abp.io/en/abp/4.2/UI/Angular/Page-Alerts) for the **Angular UI**. +* Implemented [Page Progress](https://docs.abp.io/en/abp/4.2/UI/Blazor/Page-Progress) for the **Blazor UI**. It automatically shows an undetermined progress bar on top of the page while performing an AJAX request. It also proves an API to you if you need to show/hide the progress bar in your code. + +## What's new with the ABP Commercial 4.2 + +### Microservice Startup Template + +The new [Microservice Startup Template](https://docs.abp.io/en/commercial/4.2/startup-templates/microservice/index) is a generic solution to start a new microservice solution. + +While we accept that every microservice solution will be different and every system has its own design requirements and trade-offs, we believe such a startup solution is a very useful starting point for most of the solutions, and a useful example for others. + +![microservice-template-diagram](microservice-template-diagram.png) + +*Figure: A simplified overall diagram of the microservice solution.* + +You can [follow the documentation](https://docs.abp.io/en/commercial/4.2/startup-templates/microservice/index) to get started with this startup template. **This template should be considered as an early release**. We will improve it and write a lot of guides. + +If you want to use the ABP Suite to create your solution, then you need to first upgrade it: + +````bash +abp suite update +```` + +If you want, you can directly create a new solution from the command line: + +````bash +abp new Volosoft.MyMicroserviceSystem -t microservice-pro --preview +```` + +Company Name is optional. Solution name could be *MyMicroserviceSystem* for this example. + +### Public Website in the Startup Templates + +As mentioned in the previous release post, we've added a *Public Website* application to the startup templates. It is configured to authenticate through the IdentityServer with a single sign-on system. + +You can use this application to create a landing page for your actual application or a corporate website for your business. An example screenshot: + +![public-website](public-website.jpg) + +It uses the same *Lepton Theme*, so you can apply [all the styles](https://commercial.abp.io/themes). The Public Website has a different layout and also has a different setting for the styling (that can be configured in the *Settings / Lepton Theme* page of the main web application). + +> *Public Website* is optional and you need to select the "Public Website" option while creating a new solution using the ABP Suite, or use the `--with-public-website` option while using the `abp new` CLI command. + +### Easy CRM Blazor UI + +[Easy CRM](https://docs.abp.io/en/commercial/latest/samples/easy-crm) is an example application built with the ABP Commercial. MVC (Razor Pages) and Angular UI implementations were already provided. With the version 4.2, we are providing the Blazor UI implementation for this application. + +![easy-crm](easy-crm.png) + +### Other News + +* Implemented Iyzico as a payment gateway provider for the [payment module](https://commercial.abp.io/modules/Volo.Payment) in addition to Paypal, Stripe, 2Checkout and Payu providers. +* ABP Suite supports the new microservice template creation, public website and DBMS selection options. +* Swagger authorization and other features mentioned in the ABP Framework section are already implemented for the ABP Commercial too. + +## ABP Community News + +### Sharing Video Contents + +[community.abp.io](https://community.abp.io/) is a place to share ABP related contents. It started with publishing articles. Now, it supports to publish video contents. [See this example](https://community.abp.io/articles/be-a-superhero-on-day-1-with-abp.io-wvifcy9s). All you need to do is to create a video and upload to YouTube. Then you can [submit](https://community.abp.io/articles/submit) the YouTube link to the ABP Community website. + +### Multi-language support + +We planned ABP Community to publish English-only contents. However, we see that people want to share contents in other languages too. Now, **it is possible to submit a content in any language**. Just select the Language option while submitting your content. + +**When you submit a non-English content, it is not visible to all the visitors by default**. Visitors can see a non-English content only if their browser language or the selected language matches to the content language (there is a language selection at the end of the website). + +### External Contents + +If you want to publish your content anywhere else, but want to post a link of your content, you can select *External Content* option while submitting the post. For example, [this article](https://community.abp.io/articles/aspnet-boilerplate-to-abp-framework-xml-to-json-localization-conversion-0mxyjrzj) is an external article and also written in Chinese language. + +## About the Next Release + +The next feature version will be 4.3.0. It is planned to release the 4.3 RC (Release Candidate) on March 11 and the final version on March 25, 2021. + +We decided to slow down the feature development for the [next milestone](https://github.com/abpframework/abp/milestone/49). We will continue to improve the existing features and introduce new ones, sure, but wanted to have more time for the planning, documentation, creating guides and improving the development experience. + +## Feedback + +Please check out the ABP Framework 4.2.0 RC and [provide feedback](https://github.com/abpframework/abp/issues/new) to help us to release a more stable version. **The planned release date for the [4.2.0 final](https://github.com/abpframework/abp/milestone/48) version is January 28, 2021**. diff --git a/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/easy-crm.png b/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/easy-crm.png new file mode 100644 index 0000000000..02c47777a4 Binary files /dev/null and b/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/easy-crm.png differ diff --git a/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/microservice-template-diagram.png b/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/microservice-template-diagram.png new file mode 100644 index 0000000000..2c6df5ab74 Binary files /dev/null and b/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/microservice-template-diagram.png differ diff --git a/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/public-website.jpg b/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/public-website.jpg new file mode 100644 index 0000000000..b5784519bd Binary files /dev/null and b/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/public-website.jpg differ diff --git a/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/swagger-authorize-modal.png b/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/swagger-authorize-modal.png new file mode 100644 index 0000000000..52b43f248e Binary files /dev/null and b/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/swagger-authorize-modal.png differ diff --git a/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/swagger-authorize.png b/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/swagger-authorize.png new file mode 100644 index 0000000000..650a212fe5 Binary files /dev/null and b/docs/en/Blog-Posts/2021-01-14 v4_2_Preview/swagger-authorize.png differ diff --git a/docs/en/CLI.md b/docs/en/CLI.md index 8713bc9019..fc76aa997b 100644 --- a/docs/en/CLI.md +++ b/docs/en/CLI.md @@ -41,6 +41,7 @@ Here, the list of all available commands before explaining their details: * **`login`**: Authenticates on your computer with your [abp.io](https://abp.io/) username and password. * **`logout`**: Logouts from your computer if you've authenticated before. * **`build`**: Builds a GIT repository and depending repositories or a single .NET solution. +* **`bundle`**: Generates script and style references for an ABP Blazor project. ### help @@ -104,8 +105,15 @@ abp new Acme.BookStore * `--preview`: Use latest preview version. * `--template-source` or `-ts`: Specifies a custom template source to use to build the project. Local and network sources can be used(Like `D:\local-template` or `https://.../my-template-file.zip`). * `--create-solution-folder` or `-csf`: Specifies if the project will be in a new folder in the output folder or directly the output folder. -* `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true` for EF Core and it is configured to use the SQL Server. If you want to use the EF Core, but need to change the DBMS, you can change it as [described here](Entity-Framework-Core-Other-DBMS.md) (after creating the solution). +* `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True` for EF Core and it is configured to use the SQL Server. If you want to use the EF Core, but need to change the DBMS, you can change it as [described here](Entity-Framework-Core-Other-DBMS.md) (after creating the solution). +* `--database-management-system` or `-dbms`: Sets the database management system. Default is **SQL Server**. Supported DBMS's: + * `SqlServer` + * `MySQL` + * `SQLite` + * `Oracle-Devart` + * `PostgreSQL` * `--local-framework-ref --abp-path`: Uses local projects references to the ABP framework instead of using the NuGet packages. This can be useful if you download the ABP Framework source code and have a local reference to the framework from your application. +* `--no-random-port`: Uses template's default ports. ### update @@ -127,6 +135,7 @@ abp update [options] * `--solution-path` or `-sp`: Specify the solution path. Use the current directory by default * `--solution-name` or `-sn`: Specify the solution name. Search `*.sln` files in the directory by default. * `--check-all`: Check the new version of each package separately. Default is `false`. +* `--version` or `-v`: Specifies the version to use for update. If not specified, latest version is used. ### add-package @@ -159,6 +168,8 @@ abp add-package Volo.Abp.MongoDB Adds a [multi-package application module](Modules/Index) to a solution by finding all packages of the module, finding related projects in the solution and adding each package to the corresponding project in the solution. +It can also create a new module for your solution and add it to your solution. See `--new` option. + > A business module generally consists of several packages (because of layering, different database provider options or other reasons). Using `add-module` command dramatically simplifies adding a module to a solution. However, each module may require some additional configurations which is generally indicated in the documentation of the related module. Usage @@ -167,21 +178,29 @@ Usage abp add-module [options] ```` -Example: +Examples: ```bash abp add-module Volo.Blogging ``` -* This example add the Volo.Blogging module to the solution. +* This example adds the `Volo.Blogging` module to the solution. + +```bash +abp add-module ProductManagement --new --add-to-solution-file +``` + +* This command creates a fresh new module customized for your solution (named `ProductManagement`) and adds it to your solution. + #### Options * `--solution` or `-s`: Specifies the solution (.sln) file path. If not specified, CLI tries to find a .sln file in the current directory. * `--skip-db-migrations`: For EF Core database provider, it automatically adds a new code first migration (`Add-Migration`) and updates the database (`Update-Database`) if necessary. Specify this option to skip this operation. * `-sp` or `--startup-project`: Relative path to the project folder of the startup project. Default value is the current folder. -* `--with-source-code`: Downloads the source code of the module to your solution folder and uses local project references instead of NuGet/NPM packages. -* `--add-to-solution-file`: Adds the downloaded module to your solution file, so you will also see the projects of the module when you open the solution on a IDE. (only available when `--with-source-code` is used.) +* `--new`: Creates a fresh new module (customized for your solution) and adds it to your solution. +* `--with-source-code`: Downloads the source code of the module to your solution folder and uses local project references instead of NuGet/NPM packages. This options is always `True` if `--new` is used. +* `--add-to-solution-file`: Adds the downloaded/created module to your solution file, so you will also see the projects of the module when you open the solution on a IDE. (only available when `--with-source-code` is `True`.) ### get-source @@ -206,7 +225,7 @@ abp get-source Volo.Blogging --local-framework-ref --abp-path D:\GitHub\abp * `--output-folder` or `-o`: Specifies the directory that source code will be downloaded in. If not specified, current directory is used. * `--version` or `-v`: Specifies the version of the source code that will be downloaded. If not specified, latest version is used. * `--preview`: If no version option is specified, this option specifies if latest [preview version](Previews.md) will be used instead of latest stable version. -* `-- local-framework-ref --abp-path`: Path of [ABP Framework GitHub repository](https://github.com/abpframework/abp) in your computer. This will be used for converting project references to your local system. If this is not specified, project references will be converted to NuGet references. +* `--local-framework-ref --abp-path`: Path of [ABP Framework GitHub repository](https://github.com/abpframework/abp) in your computer. This will be used for converting project references to your local system. If this is not specified, project references will be converted to NuGet references. ### generate-proxy @@ -383,10 +402,28 @@ abp build --build-name "prod" --dotnet-build-arguments "\"--no-dependencies\"" #### Options -* ```--working-directory``` or ```-w```: Specifies the working directory. This option is useful when the command is executed outside of a GIT repository or when executing directory doesn't contain a .NET solution file. +* ```--working-directory``` or ```-wd```: Specifies the working directory. This option is useful when the command is executed outside of a GIT repository or when executing directory doesn't contain a .NET solution file. * ```--build-name``` or ```-n```: Specifies a name for the build. This option is useful when same repository is used for more than one different builds. * ```--dotnet-build-arguments``` or ```-a```: Arguments to pass ```dotnet build``` when building project files. This parameter must be passed like ```"\"{params}\""``` . * ```--force``` or ```-f```: Forces to build projects even they are not changed from the last successful build. For more details, see [build command documentation](CLI-BuildCommand.md). + +#### bundle + +This command generates script and style references for an ABP Blazor project and updates the **index.html** file. It helps developers to manage dependencies required by ABP modules easily. In order ```bundle``` command to work, its **executing directory** or passed ```--working-directory``` parameter's directory must contain a Blazor project file(*.csproj). + +Usage: + +````bash +abp bundle [options] +```` + +#### Options + +* ```--working-directory``` or ```-wd```: Specifies the working directory. This option is useful when executing directory doesn't contain a Blazor project file. +* ```--force``` or ```-f```: Forces to build project before generating references. + +`bundle` command reads the `appsettings.json` file inside the Blazor project for bundling options. For more details about managing style and script references in Blazor apps, see [Managing Global Scripts & Styles](UI/Blazor/Global-Scripts-Styles.md) + diff --git a/docs/en/CSRF-Anti-Forgery.md b/docs/en/CSRF-Anti-Forgery.md index ad838e3548..ee684aaa52 100644 --- a/docs/en/CSRF-Anti-Forgery.md +++ b/docs/en/CSRF-Anti-Forgery.md @@ -25,7 +25,7 @@ ABP Framework provides `[AbpValidateAntiForgeryToken]` and `[AbpAutoValidateAnti ABP Framework also automates the following infrastructure; -* Server side sets a **special cookie**, named `XSRF-TOKEN` by default, that is used make the antiforgery token value available to the browser. This is **done automatically** (by the [application configuration](Application-Configuration.md) endpoint). Nothing to do in the client side. +* Server side sets a **special cookie**, named `XSRF-TOKEN` by default, that is used make the antiforgery token value available to the browser. This is **done automatically** (by the [application configuration](API/Application-Configuration.md) endpoint). Nothing to do in the client side. * In the client side, it reads the token from the cookie and sends it in the **HTTP header** (named `RequestVerificationToken` by default). This is implemented for all the supported UI types. * Server side validates the antiforgery token **only for same and cross site requests** made by the browser. It bypasses the validation for non-browser clients. @@ -117,3 +117,46 @@ You don't need to make anything unless you need to change the `AntiforgeryOption }) export class AppModule {} ``` + +**Note:** XSRF-TOKEN is only valid if both frontend application and APIs run on the same domain. Therefore, when you make a request, you should use a relative path. + +For example, let's say your APIs is hosted at `https://testdomain.com/ws` +and your angular application is hosted at `https://testdomain.com/admin` + +So if your API request should look like this `https://testdomain.com/ws/api/identity/users` + +your `environment.prod.ts` has to be as follows: + +```typescript +export const environment = { + production: true, + // .... + apis: { + default: { + url: '/ws', // <- just use the context root here + // ... + }, + }, +} as Config.Environment; +``` + +Let's talk about why. + +First, take a look at [Angular's code](https://github.com/angular/angular/blob/master/packages/common/http/src/xsrf.ts#L81) + +It does not intercept any request that starts with `http://` or `https://`. There is a good reason for that. Any cross-site request does not need this token for security. This verification is only valid if the request is made to the same domain from which the web page is served. So, simply put, if you serve everything from a single domain, you just use a relative path. + +If you serve your APIs from the root, i.e. no context root (https://testdomain.com/api/identity/users), leave `url` empty as follows: + +```typescript +export const environment = { + production: true, + // .... + apis: { + default: { + url: '', // <- should be empty string, not '/' + // ... + }, + }, +} as Config.Environment; +``` diff --git a/docs/en/Community-Articles/2020-04-19-Customize-the-SignIn-Manager/POST.md b/docs/en/Community-Articles/2020-04-19-Customize-the-SignIn-Manager/POST.md index d46049526d..9ae426624f 100644 --- a/docs/en/Community-Articles/2020-04-19-Customize-the-SignIn-Manager/POST.md +++ b/docs/en/Community-Articles/2020-04-19-Customize-the-SignIn-Manager/POST.md @@ -1,6 +1,6 @@ # How to Customize the SignIn Manager for ABP Applications -After creating a new application using the [application startup template](https://docs.abp.io/en/abp/latest/Startup-Templates/Application), you may want extend or change the default behavior of the SignIn Manager for your authentication and registration flow needs. ABP [Account Module](https://docs.abp.io/en/abp/latest/Modules/Account) uses the [Identity Management Module](https://docs.abp.io/en/abp/latest/Modules/Identity) for SignIn Manager and the [Identity Management Module](https://docs.abp.io/en/abp/latest/Modules/Identity) uses default [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) ([see here](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17)). +After creating a new application using the [application startup template](https://docs.abp.io/en/abp/latest/Startup-Templates/Application), you may want extend or change the default behavior of the SignIn Manager for your authentication and registration flow needs. ABP [Account Module](https://docs.abp.io/en/abp/latest/Modules/Account) uses the [Identity Management Module](https://docs.abp.io/en/abp/latest/Modules/Identity) for SignIn Manager and the [Identity Management Module](https://docs.abp.io/en/abp/latest/Modules/Identity) uses default [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) ([see here](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17)). To write your Custom SignIn Manager, you need to extend [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) class and register it to the DI container. @@ -27,7 +27,7 @@ public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager It is important to use **Volo.Abp.Identity.IdentityUser** type for SignInManager to inherit, not the AppUser of your application. +> It is important to use **Volo.Abp.Identity.IdentityUser** type for SignInManager to inherit, not the AppUser of your application. Afterwards you can override any of the SignIn Manager methods you need and add new methods and properties needed for your authentication or registration flow. @@ -38,7 +38,7 @@ In this case we'll be overriding the `GetExternalLoginInfoAsync` method which is A good way to override a method is copying its [source code](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Identity/Core/src/SignInManager.cs#L638-L674). In this case, we will be using a minorly modified version of the source code which explicitly shows the namespaces of the methods and properties to help better understanding of the concept. ````csharp -public override async Task GetExternalLoginInfoAsync(string expectedXsrf = null) +public async override Task GetExternalLoginInfoAsync(string expectedXsrf = null) { var auth = await Context.AuthenticateAsync(Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme); var items = auth?.Properties?.Items; @@ -71,7 +71,8 @@ public override async Task GetE ?? provider; return new Microsoft.AspNetCore.Identity.ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) { - AuthenticationTokens = auth.Properties.GetTokens() + AuthenticationTokens = auth.Properties.GetTokens(), + AuthenticationProperties = auth.Properties }; } ```` @@ -93,4 +94,4 @@ PreConfigure(identityBuilder => ## The Source Code -You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization). \ No newline at end of file +You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization). diff --git a/docs/en/Community-Articles/2020-04-27-Use-Azure-Active-Directory-Authentication-for-MVC-Razor-Page-Applications/POST.md b/docs/en/Community-Articles/2020-04-27-Use-Azure-Active-Directory-Authentication-for-MVC-Razor-Page-Applications/POST.md index c7b0258d55..146d250d1f 100644 --- a/docs/en/Community-Articles/2020-04-27-Use-Azure-Active-Directory-Authentication-for-MVC-Razor-Page-Applications/POST.md +++ b/docs/en/Community-Articles/2020-04-27-Use-Azure-Active-Directory-Authentication-for-MVC-Razor-Page-Applications/POST.md @@ -6,9 +6,9 @@ Adding Azure Active Directory is pretty straightforward in ABP framework. Couple Two different **alternative approaches** for AzureAD integration will be demonstrated for better coverage. -1. **AddAzureAD**: This approach uses Microsoft [AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/) which is very popular when users search the web about how to integrate AzureAD to their web application. - +1. ~~**AddAzureAD**: This approach uses Microsoft [AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/) which is very popular when users search the web about how to integrate AzureAD to their web application.~~ Now marked **Obsolete** (see https://github.com/aspnet/Announcements/issues/439). 2. **AddOpenIdConnect**: This approach uses default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for not only AzureAD but for all OpenId connections. +3. **AddMicrosoftIdentityWebAppAuthentication:** This approach uses newly introduced [Microsoft.Identity.Web nuget package](https://www.nuget.org/packages/Microsoft.Identity.Web/) to replace AddAzureAD. > There is **no difference** in functionality between these approaches. AddAzureAD is an abstracted way of OpenIdConnection ([source](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADAuthenticationBuilderExtensions.cs#L122)) with predefined cookie settings. > @@ -134,13 +134,50 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi And that's it, integration is completed. Keep on mind that you can connect any other external authentication providers. +## 3. AddMicrosoftIdentityWebAppAuthentication + +With .Net 5.0, AzureAd is marked [obsolete](https://github.com/dotnet/aspnetcore/issues/25807) and will not be supported in the near future. However its expanded functionality is available in [microsoft-identity-web](https://github.com/AzureAD/microsoft-identity-web/wiki) packages. + +Add (or replace with) the new nuget package Microsoft.Identity.Web nuget package](https://www.nuget.org/packages/Microsoft.Identity.Web/). + +In your **.Web** project; you update the `ConfigureAuthentication` method located in your **ApplicationWebModule** with the following while having the AzureAd appsettings section as defined before: + +````csharp +private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) + { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); + context.Services.AddAuthentication() + .AddIdentityServerAuthentication(options => + { + options.Authority = configuration["AuthServer:Authority"]; + options.RequireHttpsMetadata = false; + options.ApiName = "Acme.BookStore"; + }); + + context.Services.AddMicrosoftIdentityWebAppAuthentication( + configuration: configuration, + configSectionName: "AzureAd", + openIdConnectScheme:"AzureAD", + cookieScheme:null); + } +```` + +And that's all to add new Microsoft-Identity-Web. + +> **Don't forget to:** +> +> * Pass **cookieScheme** parameter as **null** or your [*GetExternalLoginInfoAsync* method will always return null](https://github.com/AzureAD/microsoft-identity-web/issues/133#). + +Keep in mind that [Microsoft-Identity-Web](https://github.com/AzureAD/microsoft-identity-web) is relatively new and keeps getting new enhancements, features and documentation. + ## The Source Code You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization). # FAQ -* Help! `GetExternalLoginInfoAsync` returns `null`! +* Help! `GetExternalLoginInfoAsync` returns `null`! (Using obsolute **AddAzureAD**) * There can be 2 reasons for this; @@ -158,6 +195,11 @@ You can find the source code of the completed example [here](https://github.com/ ```` +* Help! `GetExternalLoginInfoAsync` returns `null`! (Using **AddMicrosoftIdentityWebAppAuthentication**) + + + * Pass cookieScheme parameter as **null**. (See [this issue](https://github.com/AzureAD/microsoft-identity-web/issues/133)). + * Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error! diff --git a/docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/POST.md b/docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/POST.md index b31ea023f2..9489c004bd 100644 --- a/docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/POST.md +++ b/docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/POST.md @@ -33,7 +33,7 @@ namespace PasswordlessAuthentication.Web } //We need to override this method as well. - public override async Task GetUserModifierAsync(string purpose, UserManager manager, TUser user) + public async override Task GetUserModifierAsync(string purpose, UserManager manager, TUser user) { var userId = await manager.GetUserIdAsync(user); @@ -105,7 +105,7 @@ namespace PasswordlessAuthentication.Web.Pages UserManager = userManager; _userRepository = userRepository; } - + public ActionResult OnGet() { if (!CurrentUser.IsAuthenticated) @@ -115,15 +115,15 @@ namespace PasswordlessAuthentication.Web.Pages return Page(); } - + //added for passwordless authentication public async Task OnPostGeneratePasswordlessTokenAsync() { var adminUser = await _userRepository.FindByNormalizedUserNameAsync("admin"); - + var token = await UserManager.GenerateUserTokenAsync(adminUser, "PasswordlessLoginProvider", "passwordless-auth"); - + PasswordlessLoginUrl = Url.Action("Login", "Passwordless", new {token = token, userId = adminUser.Id.ToString()}, Request.Scheme); @@ -238,7 +238,7 @@ namespace PasswordlessAuthentication.Web.Controllers return Redirect("/"); } - + private static IEnumerable CreateClaims(IUser user, IEnumerable roles) { var claims = new List @@ -272,4 +272,4 @@ That's all! We created a passwordless login with 7 steps. ## Source Code -The completed sample is available on [GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication). \ No newline at end of file +The completed sample is available on [GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication). diff --git a/docs/en/Community-Articles/2020-08-12-Patch-Chrome-Login-Issue-For-IdentityServer4/POST.md b/docs/en/Community-Articles/2020-08-12-Patch-Chrome-Login-Issue-For-IdentityServer4/POST.md index 718a949f3a..d0d2b1ccee 100644 --- a/docs/en/Community-Articles/2020-08-12-Patch-Chrome-Login-Issue-For-IdentityServer4/POST.md +++ b/docs/en/Community-Articles/2020-08-12-Patch-Chrome-Login-Issue-For-IdentityServer4/POST.md @@ -11,157 +11,75 @@ When you use HTTP on your Identity Server 4 enabled website, users may not login Create the below extension in your ***.Web** project. ```csharp -using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.DependencyInjection { public static class SameSiteCookiesServiceCollectionExtensions { - /// - /// -1 defines the unspecified value, which tells ASPNET Core to NOT - /// send the SameSite attribute. With ASPNET Core 3.1 the - /// enum will have a definition for - /// Unspecified. - /// - private const SameSiteMode Unspecified = (SameSiteMode)(-1); - - /// - /// Configures a cookie policy to properly set the SameSite attribute - /// for Browsers that handle unknown values as Strict. Ensure that you - /// add the - /// into the pipeline before sending any cookies! - /// - /// - /// Minimum ASPNET Core Version required for this code: - /// - 2.1.14 - /// - 2.2.8 - /// - 3.0.1 - /// - 3.1.0-preview1 - /// Starting with version 80 of Chrome (to be released in February 2020) - /// cookies with NO SameSite attribute are treated as SameSite=Lax. - /// In order to always get the cookies send they need to be set to - /// SameSite=None. But since the current standard only defines Lax and - /// Strict as valid values there are some browsers that treat invalid - /// values as SameSite=Strict. We therefore need to check the browser - /// and either send SameSite=None or prevent the sending of SameSite=None. - /// Relevant links: - /// - https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1 - /// - https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 - /// - https://www.chromium.org/updates/same-site - /// - https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/ - /// - https://bugs.webkit.org/show_bug.cgi?id=198181 - /// - /// The service collection to register into. - /// The modified . - public static IServiceCollection ConfigureNonBreakingSameSiteCookies(this IServiceCollection services) + public static IServiceCollection AddSameSiteCookiePolicy(this IServiceCollection services) { services.Configure(options => { - options.MinimumSameSitePolicy = Unspecified; - options.OnAppendCookie = cookieContext => - CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); - options.OnDeleteCookie = cookieContext => - CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); + options.MinimumSameSitePolicy = SameSiteMode.Unspecified; + options.OnAppendCookie = cookieContext => + CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); + options.OnDeleteCookie = cookieContext => + CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); }); return services; } - + private static void CheckSameSite(HttpContext httpContext, CookieOptions options) { if (options.SameSite == SameSiteMode.None) { var userAgent = httpContext.Request.Headers["User-Agent"].ToString(); - - if (DisallowsSameSiteNone(userAgent)) + if (!httpContext.Request.IsHttps || DisallowsSameSiteNone(userAgent)) { - options.SameSite = Unspecified; + // For .NET Core < 3.1 set SameSite = (SameSiteMode)(-1) + options.SameSite = SameSiteMode.Unspecified; } } } - /// - /// Checks if the UserAgent is known to interpret an unknown value as Strict. - /// For those the property should be - /// set to . - /// - /// - /// This code is taken from Microsoft: - /// https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/ - /// - /// The user agent string to check. - /// Whether the specified user agent (browser) accepts SameSite=None or not. private static bool DisallowsSameSiteNone(string userAgent) { // Cover all iOS based browsers here. This includes: - // - Safari on iOS 12 for iPhone, iPod Touch, iPad - // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad - // - Chrome on iOS 12 for iPhone, iPod Touch, iPad - // All of which are broken by SameSite=None, because they use the - // iOS networking stack. - // Notes from Thinktecture: - // Regarding https://caniuse.com/#search=samesite iOS versions lower - // than 12 are not supporting SameSite at all. Starting with version 13 - // unknown values are NOT treated as strict anymore. Therefore we only - // need to check version 12. - if (userAgent.Contains("CPU iPhone OS 12") - || userAgent.Contains("iPad; CPU OS 12")) + // - Safari on iOS 12 for iPhone, iPod Touch, iPad + // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad + // - Chrome on iOS 12 for iPhone, iPod Touch, iPad + // All of which are broken by SameSite=None, because they use the iOS networking stack + if (userAgent.Contains("CPU iPhone OS 12") || userAgent.Contains("iPad; CPU OS 12")) { return true; } - // Cover Mac OS X based browsers that use the Mac OS networking stack. - // This includes: - // - Safari on Mac OS X. + // Cover Mac OS X based browsers that use the Mac OS networking stack. This includes: + // - Safari on Mac OS X. // This does not include: - // - Chrome on Mac OS X - // because they do not use the Mac OS networking stack. - // Notes from Thinktecture: - // Regarding https://caniuse.com/#search=samesite MacOS X versions lower - // than 10.14 are not supporting SameSite at all. Starting with version - // 10.15 unknown values are NOT treated as strict anymore. Therefore we - // only need to check version 10.14. - if (userAgent.Contains("Safari") - && userAgent.Contains("Macintosh; Intel Mac OS X 10_14") - && userAgent.Contains("Version/")) + // - Chrome on Mac OS X + // Because they do not use the Mac OS networking stack. + if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") && + userAgent.Contains("Version/") && userAgent.Contains("Safari")) { return true; } - // Cover Chrome 50-69, because some versions are broken by SameSite=None + // Cover Chrome 50-69, because some versions are broken by SameSite=None, // and none in this range require it. - // Note: this covers some pre-Chromium Edge versions, + // Note: this covers some pre-Chromium Edge versions, // but pre-Chromium Edge does not require SameSite=None. - // Notes from Thinktecture: - // We can not validate this assumption, but we trust Microsofts - // evaluation. And overall not sending a SameSite value equals to the same - // behavior as SameSite=None for these old versions anyways. if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6")) { return true; } - - if (GetChromeVersion(userAgent) >= 80) - { - return true; - } return false; } - - private static int GetChromeVersion(string userAgent) - { - try - { - return Convert.ToInt32(userAgent.Split("Chrome/")[1].Split('.')[0]); - } - catch (Exception) - { - return 0; - } - } } } ``` @@ -173,7 +91,7 @@ Assume that your project name is *Acme.BookStore*. Then open `AcmeBookStoreWebMo Add the following line to `ConfigureServices()` method. ```csharp - context.Services.ConfigureNonBreakingSameSiteCookies(); + context.Services.AddSameSiteCookiePolicy(); // cookie policy to deal with temporary browser incompatibilities ``` ### Step-3 @@ -195,18 +113,14 @@ public override void OnApplicationInitialization(ApplicationInitializationContex app.UseHsts(); } - app.UseCookiePolicy(); //<--- added this ---> + app.UseCookiePolicy(); // added this, Before UseAuthentication or anything else that writes cookies. //.... } ``` - - It's all! You are ready to go! - - --- -Referenced from https://www.thinktecture.com/en/identity/samesite/prepare-your-identityserver/ +Referenced from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/ diff --git a/docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/POST.md b/docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/POST.md index 7bfd20637f..3fd6b845a9 100644 --- a/docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/POST.md +++ b/docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/POST.md @@ -22,6 +22,8 @@ Then add a string property called `Title`, as an example property. ### Create AppUserDto +_Note that, creating `AppUserDto` is not necessary after ABP v4.X_ + ABP Suite needs a DTO for the target entity (user, in this case) in order to define a navigation property. To do this, create a new folder called "Users" in `*.Application.Contracts` then add a new class called `AppUserDto` inherited from `IdentityUserDto`. diff --git a/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/POST.md b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/POST.md index b286bb509d..c7f4f40917 100644 --- a/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/POST.md +++ b/docs/en/Community-Articles/2020-09-16-How-to-Setup-Azure-Active-Directory-and-Integrate-Abp-Angular-Application/POST.md @@ -1,22 +1,22 @@ -# How to Setup Azure Active Directory and Integrate Abp Angular Application +# How to Setup Azure Active Directory and Integrate ABP Angular Application -This guide demonstrates how to register an application to Azure Active Directory and integrate AzureAD to an ABP angular application that enables users to sign in using OAuth 2.0 with credentials from **Azure Active Directory**. +This guide demonstrates how to register an application to Azure Active Directory and integrate AzureAD to an ABP Angular application that enables users to sign in using OAuth 2.0 with credentials from **Azure Active Directory**. ## Authentication Flow -Abp angular applications use **Authentication Code with PKCE** (specs [here](https://tools.ietf.org/html/rfc7636)) which is the most suitable flow for spa applications by the time this article is written since implicit flow is deprecated. +ABP Angular application uses **Authentication Code with PKCE** (specs [here](https://tools.ietf.org/html/rfc7636)) which is the most suitable flow for SPA applications by the time this article is written since implicit flow is deprecated. The most common question is; -> Where to put OpenId connection code in angular project? +> Where to put OpenId connection code in the Angular project? -The answer is, **you don't**. Abp angular application is integrated with backend code (HttpApi.Host project) where it loads the configurations, **permissions** etc. For none-tiered angular applications, **HttpApi.Host** project also has IdentityServer4 embedded; also serving as **Authorization Server**. Angular application authentication flow is shown below. +The answer is, **you don't**. ABP Angular application is integrated with the backend (HttpApi.Host project) where it loads the configurations, **permissions** etc. For none-tiered angular applications, **HttpApi.Host** project also has IdentityServer4 embedded; also serving as **Authorization Server**. Angular application authentication flow is shown below. auth-diagram > What if I want Azure AD as my authorization server and not IdentityServer? -This means your application will be using AzureAD user store for authentication. By registering both angular app and HttpApi to AzureAD, authentication might work but **authorization won't**. Users need to be registered to Abp identity system for auditing, permissions etc. So the flow should be 3rd party registration. +This means your application will be using AzureAD user store for authentication. By registering both Angular app and HttpApi to AzureAD, authentication might work but **authorization won't**. Users need to be registered to ABP identity system for auditing, permissions etc. So the flow should be 3rd party registration. ## Setting up OpenId Connection diff --git a/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/POST.md b/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/POST.md new file mode 100644 index 0000000000..f1387b2a50 --- /dev/null +++ b/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/POST.md @@ -0,0 +1,46 @@ +# How to add a new language to your ABP project? + +Adding a new language to your ABP project is pretty simple. Let's add the German language to our ABP project: + + + +1. Go to your solution's root folder and write the following CLI command. This command will generate an empty translation file from English. + ```bash + abp translate -c de-DE + ``` + + Check out for [the complete supported culture codes](https://docs.microsoft.com/en-us/bingmaps/rest-services/common-parameters-and-types/supported-culture-codes). + (For internal development `D:\Github\abp` and `D:\Github\volo\abp`) + +2. Fill the `target` fields in your target language. + + ![Fill target fields](language-target.png) + +3. Copy `abp-translation.json` your solution's root folder (Do not change the filename!) + +4. Run the following command. This command will create the necessary `json` files. + ```bash + abp translate --apply + ``` + +5. Open your solution and add the new language to the language list. To do this; + + * open `MyProjectNameDomainModule.cs` and in `ConfigureServices` you'll find `Configure`. If you have `HttpApi.Host` project then you need to add this in `MyProjectNameHttpApiHostModule.cs` + + ``` + options.Languages.Add(new LanguageInfo("de-DE", "de-DE", "Deutsch", "de")); + ``` + + ![Add to languages](add-to-languages.png) + + The last parameter is the flag icon. You can find the list of flag icons on https://flagicons.lipis.dev/ + + 6. The last step is running the DbMigrator project. It will seed the database for the new language. + ![The database table](database-table.png) + + + +Close the IIS Express / Kestrel to invalidate the language cache and run the project. You will see the new language on your website. + +![See the final result](website-new-language.png) + diff --git a/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/add-to-languages.png b/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/add-to-languages.png new file mode 100644 index 0000000000..1dda317685 Binary files /dev/null and b/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/add-to-languages.png differ diff --git a/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/database-table.png b/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/database-table.png new file mode 100644 index 0000000000..2d512fabeb Binary files /dev/null and b/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/database-table.png differ diff --git a/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/language-target.png b/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/language-target.png new file mode 100644 index 0000000000..7b5f104003 Binary files /dev/null and b/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/language-target.png differ diff --git a/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/website-new-language.png b/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/website-new-language.png new file mode 100644 index 0000000000..1e04841983 Binary files /dev/null and b/docs/en/Community-Articles/2020-11-02-How-To-Add-New-Language/website-new-language.png differ diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md b/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md new file mode 100644 index 0000000000..b36d83d0ba --- /dev/null +++ b/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md @@ -0,0 +1,936 @@ +# Creating an Event Organizer Application with the ABP Framework & Blazor UI. + +## Introduction + +In this article, we will create an example application that is a simple **meeting/event organizer**: People create events and other people registers to the event. + +The application has been developed with **Blazor** as the UI framework and **MongoDB** as the database provider. + +> This tutorial is based on my notes that I'd created to implement this application in a workshop. It shows the necessary steps to build the application rather than detailed explanations. + +### Source Code + +Source code of the completed application is [available on GitHub](https://github.com/abpframework/abp-samples/tree/master/EventOrganizer). + +### Screenshots + +Here, the pages of the final application. + +**Home Page - Event List** + +![event-list-ui](images/event-list-ui.png) + +**Creating a new Event** + +![event-create-ui](images/event-create-ui.png) + +**Event Detail Page** + +![event-detail-ui](images/event-detail-ui.png) + +## Requirements + +The following tools are needed to be able to run the solution. + +* .NET 5.0 SDK +* Visual Studio 2019 16.8.0+ or another compatible IDE +* MongoDB Server (with MongoDB Compass) + +## Development + +### Creating a new Application + +* Use the following ABP CLI command: + +````bash +abp new EventOrganizer -u blazor -d mongodb +```` + +### Open & Run the Application + +* Open the solution in Visual Studio (or your favorite IDE). +* Run the `EventOrganizer.DbMigrator` application to seed the initial data. +* Run the `EventOrganizer.HttpApi.Host` application that starts the server side. +* Run the `EventOrganizer.Blazor` application to start the UI. + +### Apply the Custom Styles + +* Add styles to `wwwroot/main.css`: + +````css +body.abp-application-layout { + background-color: #222 !important; + font-size: 18px; +} +nav#main-navbar.bg-dark { + background-color: #222 !important; + box-shadow: none !important; +} +.event-pic { + width: 100%; + border-radius: 12px; + box-shadow: 5px 5px 0px 0px rgba(0,0,0,.5); + margin-bottom: 10px; +} +.event-link:hover, .event-link:hover *{ + text-decoration: none; +} +.event-link:hover .event-pic { + box-shadow: 5px 5px 0px 0px #ffd800; +} +.event-form { + background-color: #333 !important; + box-shadow: 5px 5px 0px 0px rgba(0,0,0,.5); + border-radius: 12px; +} +.table { + background: #fff; + border-radius: 12px; + box-shadow: 5px 5px 0px 0px rgba(0,0,0,.5); +} +.table th{ + border: 0 !important; +} +.modal { + color: #333; +} +.page-item:first-child .page-link { + margin-left: 0; + border-top-left-radius: 12px; + border-bottom-left-radius: 12px; +} +.page-item:last-child .page-link { + border-top-right-radius: 12px; + border-bottom-right-radius: 12px; +} +.btn { + border-radius: 8px; +} +.att-list { + list-style: none; + padding: 0; +} +.att-list li { + padding: 4px 0 0 0; +} +```` + +* `wwwroot/index.html`: Remove `bg-light` class from the `body` tag and add `bg-dark text-light`. + +### Domain Layer + +* Add the following `Event` aggregate (with `EventAttendee`) to the solution: + +**Event** + +````csharp +using System; +using System.Collections.Generic; +using Volo.Abp.Domain.Entities.Auditing; + +namespace EventOrganizer.Events +{ + public class Event : FullAuditedAggregateRoot + { + public string Title { get; set; } + + public string Description { get; set; } + + public bool IsFree { get; set; } + + public DateTime StartTime { get; set; } + + public ICollection Attendees { get; set; } + + public Event() + { + Attendees = new List(); + } + } +} +```` + +**EventAttendee** + +```csharp +using System; +using Volo.Abp.Auditing; + +namespace EventOrganizer.Events +{ + public class EventAttendee : IHasCreationTime + { + public Guid UserId { get; set; } + + public DateTime CreationTime { get; set; } + } +} +``` + +### MongoDB Mapping + +* Add the following property to the `EventOrganizerMongoDbContext`: + +````csharp +public IMongoCollection Events => Collection(); +```` + +### Clean Index.razor & Add the Header & "Create Event" button + +* Clean the `Index.razor` file. +* Replace the content with the following code: + +````html +@page "/" +@inherits EventOrganizerComponentBase + + +

Upcoming Events

+
+ + @if (CurrentUser.IsAuthenticated) + { + + @L["CreateEvent"] + + } + +
+```` + +* Open `Localization/EventOrganizer/en.json` in the `EventOrganizer.Domain.Shared` project and add the following entry: + +````json +"CreateEvent": "Create a new event!" +```` + +The Result (run the `EventOrganizer.Blazor` application to see): + +![index-title](images/index-title.png) + +### Event Creation + +* Create the Initial `IEventAppService` with the `CreateAsync` method: + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace EventOrganizer.Events +{ + public interface IEventAppService : IApplicationService + { + Task CreateAsync(EventCreationDto input); + } +} +```` + +* Add `EventCreationDto` class: + +````csharp +using System; +using System.ComponentModel.DataAnnotations; + +namespace EventOrganizer.Events +{ + public class EventCreationDto + { + [Required] + [StringLength(100)] + public string Title { get; set; } + + [Required] + [StringLength(2000)] + public string Description { get; set; } + + public bool IsFree { get; set; } + + public DateTime StartTime { get; set; } + } +} +```` + +* Implement the `EventAppService`: + +````csharp +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Volo.Abp.Domain.Repositories; + +namespace EventOrganizer.Events +{ + public class EventAppService : EventOrganizerAppService, IEventAppService + { + private readonly IRepository _eventRepository; + + public EventAppService(IRepository eventRepository) + { + _eventRepository = eventRepository; + } + + [Authorize] + public async Task CreateAsync(EventCreationDto input) + { + var eventEntity = ObjectMapper.Map(input); + await _eventRepository.InsertAsync(eventEntity); + return eventEntity.Id; + } + } +} +```` + +* Add AutoMapper mapping to the `EventOrganizerApplicationAutoMapperProfile` class: + +````csharp +using AutoMapper; +using EventOrganizer.Events; + +namespace EventOrganizer +{ + public class EventOrganizerApplicationAutoMapperProfile : Profile + { + public EventOrganizerApplicationAutoMapperProfile() + { + CreateMap(); + } + } +} +```` + +This will automatically create the HTTP (REST) API for the application service (run the `EventOrganizer.HttpApi.Host` application to see it on the Swagger UI): + +![swagger-event-create](images/swagger-event-create.png) + +* Create the `CreateEvent.razor` file: + +````csharp +@page "/create-event" +@inherits EventOrganizerComponentBase +Create Event + + +
+ + + @L["Title"] + + + + @L["Description"] + + + + @L["Free"] + + + @L["StartTime"] + + + + +
+
+
+```` + +* Create a partial `CreateEvent` class in the same folder, with the `CreateEvent.razor.cs` as the file name: + +````csharp +using System.Threading.Tasks; +using EventOrganizer.Events; +using Microsoft.AspNetCore.Components; + +namespace EventOrganizer.Blazor.Pages +{ + public partial class CreateEvent + { + private EventCreationDto Event { get; set; } = new EventCreationDto(); + + private readonly IEventAppService _eventAppService; + private readonly NavigationManager _navigationManager; + + public CreateEvent( + IEventAppService eventAppService, + NavigationManager navigationManager) + { + _eventAppService = eventAppService; + _navigationManager = navigationManager; + } + + private async Task Create() + { + var eventId = await _eventAppService.CreateAsync(Event); + _navigationManager.NavigateTo("/events/" + eventId); + } + } +} +```` + +The final UI is (run the `EventOrganizer.Blazor` application and click to the "Create Event" button): + +![event-create-ui](images/event-create-ui.png) + +### Upcoming Events (Home Page) + +* Open the `IEventAppService` and add a `GetUpcomingAsync` method to get the list of upcoming events: + +````csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace EventOrganizer.Events +{ + public interface IEventAppService : IApplicationService + { + Task CreateAsync(EventCreationDto input); + + Task> GetUpcomingAsync(); + } +} +```` + +* Add a `EventDto` class: + +````csharp +using System; +using Volo.Abp.Application.Dtos; + +namespace EventOrganizer.Events +{ + public class EventDto : EntityDto + { + public string Title { get; set; } + + public string Description { get; set; } + + public bool IsFree { get; set; } + + public DateTime StartTime { get; set; } + + public int AttendeesCount { get; set; } + } +} +```` + +* Implement the `GetUpcomingAsync` in the `EventAppService` class: + +````csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Volo.Abp.Domain.Repositories; + +namespace EventOrganizer.Events +{ + public class EventAppService : EventOrganizerAppService, IEventAppService + { + private readonly IRepository _eventRepository; + + public EventAppService(IRepository eventRepository) + { + _eventRepository = eventRepository; + } + + [Authorize] + public async Task CreateAsync(EventCreationDto input) + { + var eventEntity = ObjectMapper.Map(input); + await _eventRepository.InsertAsync(eventEntity); + return eventEntity.Id; + } + + public async Task> GetUpcomingAsync() + { + var events = await AsyncExecuter.ToListAsync( + _eventRepository + .Where(x => x.StartTime > Clock.Now) + .OrderBy(x => x.StartTime) + ); + + return ObjectMapper.Map, List>(events); + } + } +} +```` + +* Add the following line into the `EventOrganizerApplicationAutoMapperProfile` constructor: + +````csharp +CreateMap(); +```` + +Run the `EventOrganizer.HttpApi.Host` application to see the new `upcoming` endpoint on the Swagger UI: + +![swagger-event-upcoming](images/swagger-event-upcoming.png) + +* Change the `Pages/Index.razor.cs` content in the `EventOrganizer.Blazor` project as shown below: + +```csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using EventOrganizer.Events; + +namespace EventOrganizer.Blazor.Pages +{ + public partial class Index + { + private List UpcomingEvents { get; set; } = new List(); + + private readonly IEventAppService _eventAppService; + + public Index(IEventAppService eventAppService) + { + _eventAppService = eventAppService; + } + + protected override async Task OnInitializedAsync() + { + UpcomingEvents = await _eventAppService.GetUpcomingAsync(); + } + } +} +``` + +* Change the `Pages/Index.razor` content in the `EventOrganizer.Blazor` project as shown below: + +````html +@page "/" +@inherits EventOrganizerComponentBase + + +

Upcoming Events

+
+ + @if (CurrentUser.IsAuthenticated) + { + + @L["CreateEvent"] + + } + +
+ + @foreach (var upcomingEvent in UpcomingEvents) + { + + +
+ @if (upcomingEvent.IsFree) + { + FREE + } + + + @upcomingEvent.AttendeesCount + +
+ +
+ @upcomingEvent.StartTime.ToLongDateString() +

@upcomingEvent.Title

+

@upcomingEvent.Description.TruncateWithPostfix(150)

+
+
+
+ } +
+```` + +The new home page is shown below: + +![event-list-ui](images/event-list-ui.png) + +### Event Detail Page + +* Add `GetAsync`, `RegisterAsync`, `UnregisterAsync` and `DeleteAsync` methods to the `IEventAppService`: + +````csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace EventOrganizer.Events +{ + public interface IEventAppService : IApplicationService + { + Task CreateAsync(EventCreationDto input); + + Task> GetUpcomingAsync(); + + Task GetAsync(Guid id); + + Task RegisterAsync(Guid id); + + Task UnregisterAsync(Guid id); + + Task DeleteAsync(Guid id); + } +} +```` + +* Add `EventDetailDto` class: + +````csharp +using System; +using System.Collections.Generic; +using Volo.Abp.Application.Dtos; + +namespace EventOrganizer.Events +{ + public class EventDetailDto : CreationAuditedEntityDto + { + public string Title { get; set; } + + public string Description { get; set; } + + public bool IsFree { get; set; } + + public DateTime StartTime { get; set; } + + public List Attendees { get; set; } + } +} +```` + +* Add `EventAttendeeDto` class: + +````csharp +using System; + +namespace EventOrganizer.Events +{ + public class EventAttendeeDto + { + public Guid UserId { get; set; } + + public string UserName { get; set; } + + public DateTime CreationTime { get; set; } + } +} +```` + +* Implement the new methods in the `EventAppService`: + +````csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using EventOrganizer.Users; +using Microsoft.AspNetCore.Authorization; +using Volo.Abp; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Users; + +namespace EventOrganizer.Events +{ + public class EventAppService : EventOrganizerAppService, IEventAppService + { + private readonly IRepository _eventRepository; + private readonly IRepository _userRepository; + + public EventAppService(IRepository eventRepository, IRepository userRepository) + { + _eventRepository = eventRepository; + _userRepository = userRepository; + } + + [Authorize] + public async Task CreateAsync(EventCreationDto input) + { + var eventEntity = ObjectMapper.Map(input); + await _eventRepository.InsertAsync(eventEntity); + return eventEntity.Id; + } + + public async Task> GetUpcomingAsync() + { + var events = await AsyncExecuter.ToListAsync( + _eventRepository + .Where(x => x.StartTime > Clock.Now) + .OrderBy(x => x.StartTime) + ); + + return ObjectMapper.Map, List>(events); + } + + public async Task GetAsync(Guid id) + { + var @event = await _eventRepository.GetAsync(id); + var attendeeIds = @event.Attendees.Select(a => a.UserId).ToList(); + var attendees = (await AsyncExecuter.ToListAsync(_userRepository.Where(u => attendeeIds.Contains(u.Id)))) + .ToDictionary(x => x.Id); + + var result = ObjectMapper.Map(@event); + + foreach (var attendeeDto in result.Attendees) + { + attendeeDto.UserName = attendees[attendeeDto.UserId].UserName; + } + + return result; + } + + [Authorize] + public async Task RegisterAsync(Guid id) + { + var @event = await _eventRepository.GetAsync(id); + if (@event.Attendees.Any(a => a.UserId == CurrentUser.Id)) + { + return; + } + + @event.Attendees.Add(new EventAttendee {UserId = CurrentUser.GetId(), CreationTime = Clock.Now}); + await _eventRepository.UpdateAsync(@event); + } + + [Authorize] + public async Task UnregisterAsync(Guid id) + { + var @event = await _eventRepository.GetAsync(id); + var removedItems = @event.Attendees.RemoveAll(x => x.UserId == CurrentUser.Id); + if (removedItems.Any()) + { + await _eventRepository.UpdateAsync(@event); + } + } + + [Authorize] + public async Task DeleteAsync(Guid id) + { + var @event = await _eventRepository.GetAsync(id); + + if (CurrentUser.Id != @event.CreatorId) + { + throw new UserFriendlyException("You don't have the necessary permission to delete this event!"); + } + + await _eventRepository.DeleteAsync(id); + } + } +} +```` + +* Add the following mappings into the `EventOrganizerApplicationAutoMapperProfile`: + +````csharp +CreateMap(); +CreateMap(); +```` + +Run the `EventOrganizer.HttpApi.Host` application to see the complete Event HTTP API in the Swagger UI: + +![swagger-event-all](images/swagger-event-all.png) + +* Create `EventDetail.razor` component with the following content: + +````html +@page "/events/{id}" +@inherits EventOrganizerComponentBase +@if (Event != null) +{ + + +

@Event.Title

+
+ + Back + @if (CurrentUser.IsAuthenticated && CurrentUser.Id == Event.CreatorId) + { + + } + +
+ + +
+
+ @if (Event.IsFree) + { + FREE + } + + + @Event.Attendees.Count + +
+ + Start time: @Event.StartTime.ToLongDateString() +

@Event.Description

+
+
+ +
+ @if (CurrentUser.IsAuthenticated) + { +
+ @if (!IsRegistered) + { + + } + else + { +

You are registered in this event

+ + } +
+ } + else + { + + Login to attend! + + } +
+
+ Attendees (@Event.Attendees.Count) +
    + @foreach (var attendee in Event.Attendees) + { +
  • @attendee.UserName
  • + } +
+
+
+
+} +```` + +* Create `EventDetail.razor.cs` file with the following content: + +````csharp +using System; +using System.Linq; +using System.Threading.Tasks; +using EventOrganizer.Events; +using Microsoft.AspNetCore.Components; + +namespace EventOrganizer.Blazor.Pages +{ + public partial class EventDetail + { + [Parameter] + public string Id { get; set; } + + private EventDetailDto Event { get; set; } + private bool IsRegistered { get; set; } + + private readonly IEventAppService _eventAppService; + private readonly NavigationManager _navigationManager; + + public EventDetail( + IEventAppService eventAppService, + NavigationManager navigationManager) + { + _eventAppService = eventAppService; + _navigationManager = navigationManager; + } + + protected override async Task OnInitializedAsync() + { + await GetEventAsync(); + } + + private async Task GetEventAsync() + { + Event = await _eventAppService.GetAsync(Guid.Parse(Id)); + if (CurrentUser.IsAuthenticated) + { + IsRegistered = Event.Attendees.Any(a => a.UserId == CurrentUser.Id); + } + } + + private async Task Register() + { + await _eventAppService.RegisterAsync(Guid.Parse(Id)); + await GetEventAsync(); + } + + private async Task UnRegister() + { + await _eventAppService.UnregisterAsync(Guid.Parse(Id)); + await GetEventAsync(); + } + + private async Task Delete() + { + if (!await Message.Confirm("This event will be deleted: " + Event.Title)) + { + return; + } + + await _eventAppService.DeleteAsync(Guid.Parse(Id)); + _navigationManager.NavigateTo("/"); + } + } +} +```` + +The resulting page is shown below: + +![event-detail-ui](images/event-detail-ui.png) + +### Integration Tests + +Create an `EventAppService_Tests` class in the `EventOrganizer.Application.Tests` project: + +````csharp +using System; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace EventOrganizer.Events +{ + [Collection(EventOrganizerTestConsts.CollectionDefinitionName)] + public class EventAppService_Tests : EventOrganizerApplicationTestBase + { + private readonly IEventAppService _eventAppService; + + public EventAppService_Tests() + { + _eventAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Create_A_Valid_Event() + { + // Create an event + + var eventId = await _eventAppService.CreateAsync( + new EventCreationDto + { + Title = "My test event 1", + Description = "My test event description 1", + IsFree = true, + StartTime = DateTime.Now.AddDays(2) + } + ); + + eventId.ShouldNotBe(Guid.Empty); + + // Get the event + + var @event = await _eventAppService.GetAsync(eventId); + @event.Title.ShouldBe("My test event 1"); + + // Get upcoming events + + var events = await _eventAppService.GetUpcomingAsync(); + events.ShouldContain(x => x.Title == "My test event 1"); + } + } +} +```` + +## Source Code + +Source code of the completed application is [available on GitHub](https://github.com/abpframework/abp-samples/tree/master/EventOrganizer). \ No newline at end of file diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-create-ui.png b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-create-ui.png new file mode 100644 index 0000000000..c4b0c68a31 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-create-ui.png differ diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-detail-ui.png b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-detail-ui.png new file mode 100644 index 0000000000..f916a129dc Binary files /dev/null and b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-detail-ui.png differ diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-list-ui.png b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-list-ui.png new file mode 100644 index 0000000000..4f049c2339 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/event-list-ui.png differ diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/index-title.png b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/index-title.png new file mode 100644 index 0000000000..c06b8f5d80 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/index-title.png differ diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-all.png b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-all.png new file mode 100644 index 0000000000..3b0fb24a7d Binary files /dev/null and b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-all.png differ diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-create.png b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-create.png new file mode 100644 index 0000000000..5c7e1d0204 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-create.png differ diff --git a/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-upcoming.png b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-upcoming.png new file mode 100644 index 0000000000..48d0244797 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-04-Event-Organizer/images/swagger-event-upcoming.png differ diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/POST.md b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/POST.md new file mode 100644 index 0000000000..4f2f5556eb --- /dev/null +++ b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/POST.md @@ -0,0 +1,110 @@ +## Using DevExpress Blazor UI Components With the ABP Framework + +Hi, in this step by step article, I will show you how to integrate [DevExpress](https://demos.devexpress.com/blazor/) blazor UI components into ABP Framework-based applications. + +![both-example-result](both-example-result.png) + +*(A screenshot from the example application developed in this article)* + +## Create the Project + +> ABP Framework offers startup templates to get into business faster. + +In this article, I will create a new startup template with EF Core as a database provider and Blazor for UI framework. But if you already have a project with Blazor UI, you don't need to create a new startup template, you can directly implement the following steps to your existing project. + +> If you already have a project with the Blazor UI, you can skip this section. + +* Before starting to development, we will create a solution named `DevExpressSample` (or whatever you want). We will create a new startup template with EF Core as a database provider and Blazor for UI framework by using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI): + +````bash +abp new DevExpressSample -u blazor +```` + +![initial-project](initial-project.png) + +* Our project boilerplate will be ready after the download is finished. Then, we can open the solution in the Visual Studio (or any other IDE) and run the `DevExpressSample.DbMigrator` to create the database and seed initial data (which creates the admin user, admin role, permissions etc.) + +* After database and initial data created, +* Run the `DevExpressSample.HttpApi.Host` to see our server side working and +* Run the `DevExpressSample.Blazor` to see our UI working properly. + +> _Default login credentials for admin: username is **admin** and password is **1q2w3E\***_ + +## Install DevExpress + +You can follow [this documentation](https://docs.devexpress.com/Blazor/401986/getting-started/install-components-and-create-an-application/without-devexpress-installer/microsoft-templates) to install DevExpress packages into your computer. + +> Don't forget to add _"DevExpress NuGet Feed"_ to your **Nuget Package Sources**. + +### Adding DevExpress NuGet Packages + +Add the `DevExpress.Blazor` NuGet package to the `DevExpressSample.Blazor` project. + +``` +Install-Package DevExpress.Blazor +``` + +### Register DevExpress Resources + +1. Add the following line to the HEAD section of the `wwwroot/index.html` file within the `DevExpressSample.Blazor` project: + + ```Razor + + + + + ``` + +2. In the `DevExpressSampleBlazorModule` class, call the `AddDevExpressBlazor()` method from your project's `ConfigureServices()` method: + + ```csharp + public override void ConfigureServices(ServiceConfigurationContext context) + { + var environment = context.Services.GetSingletonInstance(); + var builder = context.Services.GetSingletonInstance(); + // ... + builder.Services.AddDevExpressBlazor(); + } + ``` + +3. Register the **DevExpressSample.Blazor** namespace in the `_Imports.razor` file: + + ```Razor + @using DevExpress.Blazor + ``` + +### Result + +The installation step was done. You can use any DevExpress Blazor UI component in your application: + +Example: A Scheduler: + +![sample-appointment](sample-appointment.gif) + +This example has been created by following [this documentation](https://demos.devexpress.com/blazor/SchedulerViewTypes). + +## The Sample Application + +We have created a sample application with [Data Grid](https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxDataGrid-1) example. + +### The Source Code + +You can download the source code from [here](https://github.com/abpframework/abp-samples/tree/master/DevExpress-Blazor). + +The related files for this example are marked in the following screenshots. + +![data-grid-app-contract](data-grid-app-contract.png) + +![data-grid-application](data-grid-application.png) + +![data-grid-web](data-grid-blazor.png) + +### Additional Notes + +#### Data Storage + +I've used an in-memory list to store data for this example, instead of a real database. Because it is not related to DevExpress usage. There is a `SampleDataService.cs` file in `Data` folder at `DevExpressSample.Application.Contracts` project. All the data is stored here. + +## Conclusion + +In this article, I've explained how to use [DevExpress](https://www.devexpress.com/blazor/) components in your application. ABP Framework is designed so that it can work with any UI library/framework. diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/both-example-result.png b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/both-example-result.png new file mode 100644 index 0000000000..cc437ed6e7 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/both-example-result.png differ diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/cover-image.png b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/cover-image.png new file mode 100644 index 0000000000..2dc0aed011 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/cover-image.png differ diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/data-grid-app-contract.png b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/data-grid-app-contract.png new file mode 100644 index 0000000000..536251b7b9 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/data-grid-app-contract.png differ diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/data-grid-application.png b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/data-grid-application.png new file mode 100644 index 0000000000..a169b9f9f7 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/data-grid-application.png differ diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/data-grid-blazor.png b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/data-grid-blazor.png new file mode 100644 index 0000000000..af1857a4e0 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/data-grid-blazor.png differ diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/initial-project.png b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/initial-project.png new file mode 100644 index 0000000000..c2a2af265e Binary files /dev/null and b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/initial-project.png differ diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/sample-appointment.gif b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/sample-appointment.gif new file mode 100644 index 0000000000..cde15e8f74 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/sample-appointment.gif differ diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/POST.md b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/POST.md new file mode 100644 index 0000000000..971eb88f97 --- /dev/null +++ b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/POST.md @@ -0,0 +1,537 @@ +# How to Integrate the Telerik Blazor Components to the ABP Blazor UI? + +## Introduction + +Hi, in this step by step article, we will see how we can integrate the Telerik Blazor Components to our Blazor UI. + +## Creating the Solution + +> ABP Framework offers startup templates to get into business faster. + +In this article, I will create a new startup template with EF Core as a database provider and Blazor for UI framework. But if you already have a project with Blazor UI, you don't need to create a new startup template, you can directly implement the following steps to your existing project. + +> If you already have a project with the Blazor UI, you can skip this section. + +* Before starting to development, we will create a solution named `TelerikComponents` (or whatever you want). We will create a new startup template with EF Core as a database provider and Blazor for UI framework by using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI): + +```bash +abp new TelerikComponents --ui blazor --database-provider ef +``` + +* Our project boilerplate will be ready after the download is finished. Then, we can open the solution in the Visual Studio (or any other IDE) and run the `TelerikComponents.DbMigrator` to create the database and seed initial data (which creates the admin user, admin role, permissions, etc.) + +* After the database and initial data created, +* Run the `TelerikComponents.HttpApi.Host` to see our server-side working and +* Run the `TelerikComponents.Blazor` to see our UI working. + +> _Default login credentials for admin: username is **admin** and password is **1q2w3E\***_ + +## Starting the Development + +### Pre-requisite + +* First thing we need to do is downloading the [Progress Control Panel](https://www.telerik.com/download-trial-file/v2/control-panel?_ga=2.212029332.1667119438.1607582144-1944255175.1605161949) to get Telerik Blazor Components on our development machine. + +* If you will use the Telerik Blazor Components for the first time or you don't have an active license you can click [here](https://www.telerik.com/login/v2/download-b?ReturnUrl=https%3a%2f%2fwww.telerik.com%2fdownload-trial-file%2fv2-b%2fui-for-blazor%3f_ga%3d2.212029332.1667119438.1607582144-1944255175.1605161949#register) to download free trial. + +> You can find the more installation details from [here](https://docs.telerik.com/blazor-ui/getting-started/client-blazor?_ga=2.55603115.1667119438.1607582144-1944255175.1605161949&_gac=1.261851647.1607669357.CjwKCAiAq8f-BRBtEiwAGr3DgUDhBT25rs7hU0EQ8K-AfeUVxs3hSoIuIAuBOZ17CNPI4ZEArORPExoCyd4QAvD_BwE#step-0---download-the-components). + +>**Notes:** To download Telerik Blazor packages via NuGet, we need to setup Telerik NuGet package source. We can state it in the installer as below. In this way, we can download the required Telerik Blazor packages via NuGet. + +![setup-nuget-package-source](./automated-nuget-feed-setup.png) + +### Step 1 (Configurations) + +* We need to install the `Telerik.UI.for.Blazor` Nuget package to our Blazor project (`*.Blazor`). We need to choose package source to **telerik.com** for Visual Studio to see this package. + +* If you use trial version of Telerik, you can download **Telerik.UI.for.Blazor.Trial** package via NuGet. + +* After the installation finished, we need to open **index.html** (it's under *wwwroot* folder) to add css and js files in our application. + +* Add the following lines just before the closing head tag (**/head**). + +```html + ... + + + + + +``` + +* After that, we need to add the Telerik Blazor Components to our application's service collection. So just open the `TelerikComponentsBlazorModule` and update the `ConfigureServices` method with the following content. + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var environment = context.Services.GetSingletonInstance(); + var builder = context.Services.GetSingletonInstance(); + + ConfigureAuthentication(builder); + ConfigureHttpClient(context, environment); + ConfigureBlazorise(context); + ConfigureRouter(context); + ConfigureUI(builder); + ConfigureMenu(context); + ConfigureAutoMapper(context); + + //add this line to be able to use the components + builder.Services.AddTelerikBlazor(); +} +``` + +* After the service added we will continue by opening **_Imports.razor** file and add the global using statements as below. This will bring our Telerik components into scope throughout the application. + +![telerik-blazor-component-1](./telerik-blazor-component-1.jpg) + +* After all of these steps, the Telerik UI Components are ready to be used anywhere in our application. We can use the Telerik Blazor Components by wrapping our components or pages between `` and `` tags. + +### Step 2 - Checking Configurations + +* We should check, have we done the right configurations or not. For that, we can open the **Index.razor** file and we can update the component with the following content. + +```razor +@page "/" +@using Volo.Abp.MultiTenancy +@inherits TelerikComponentsComponentBase +@inject ICurrentTenant CurrentTenant +@inject AuthenticationStateProvider AuthenticationStateProvider +@using System.Timers + +@implements IDisposable + + +
+
Telerik Progress Bar Component
+ +
+
+ +@code { + private const int TimerInterval = 1000; + private const int TotalTime = 10 * TimerInterval; + private double ProgressValue = 0; + private int ProgressStep = 100 / (TotalTime / TimerInterval); + private Timer Timer { get; set; } = new Timer(); + + private void Dispose() + { + StopProgress(); + Timer?.Close(); + } + + protected override void OnAfterRender(bool firstRender) + { + if (Timer.Enabled == false) + { + Timer.Interval = TimerInterval; + Timer.Elapsed -= OnTimerElapsed; + Timer.Elapsed += OnTimerElapsed; + Timer.AutoReset = true; + Timer.Start(); + } + } + + private void OnTimerElapsed(Object source, ElapsedEventArgs e) + { + if (ProgressValue < 100) + { + UpdateProgress(); + } + else + { + StopProgress(); + } + } + + private void UpdateProgress() + { + ProgressValue += ProgressStep; + + InvokeAsync(StateHasChanged); + } + + private void StopProgress() + { + Timer?.Stop(); + } +} + + +``` + +* In here, we've just added the `TelerikProgressBar` component to check the integration configured properly. + +* When we run `*.HttpApi.Host` and `*.Blazor` projects, we should see that the `TelerikProgressBar` component works and has similar view as the below gif. + +![telerik-progress-bar](./telerik-progress-bar.gif) + +* If you haven't seen this component like above, you should check the above configurations and be assure every step done as stated. + +### Step 3 - Using The Telerik Blazor Components (Sample Application) + +* Let's create a sample application for use other Telerik Blazor Components (like DataGrid). + +* We will use [jsonplaceholder](https://jsonplaceholder.typicode.com/) as **mock data** to the listing, adding, updating and deleting posts. + +* Firstly, we can create a folder named `Posts` and inside this folder, we can create the classes which are highlighted in the following screenshot. + +![sample-application](./sample-application.jpg) + +* After classes created we can fill the classes with the following contents. + +**Post.cs** +```csharp +using System; + +namespace TelerikComponents.Posts +{ + [Serializable] + public class Post + { + public int Id { get; set; } + + public string Title { get; set; } + + public string Body { get; set; } + + public int UserId { get; set; } + } +} +``` + +**Comment.cs** +```csharp +using System; + +namespace TelerikComponents.Posts +{ + [Serializable] + public class Comment + { + public int PostId { get; set; } + + public int Id { get; set; } + + public string Name { get; set; } + + public string Email { get; set; } + + public string Body { get; set; } + } +} +``` + +**IPostAppService.cs** +```csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace TelerikComponents.Posts +{ + public interface IPostAppService : IApplicationService + { + Task> GetPostsAsync(); + + Task AddPostAsync(Post post); + + Task UpdatePostAsync(int postId, Post post); + + Task DeletePostAsync(int postId); + + Task GetFirstCommentByPostIdAsync(int postId); + } +} +``` +* In here, we basically created two class (which are **Post** and **Comment**). These classes are used to hold data returned as JSON. + +* After that, we need to implement `IPostAppService`. For achieve this, we can create a folder named `Posts` in **\*.Application** layer and inside this folder we can create a class named `PostAppService` with the following content. + +**PostAppService.cs** +```csharp +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace TelerikComponents.Posts +{ + public class PostAppService : ApplicationService, IPostAppService + { + private JsonSerializerOptions _options = new JsonSerializerOptions + { + IncludeFields = true, + PropertyNameCaseInsensitive = true + }; + + public async Task> GetPostsAsync() + { + var url = "https://jsonplaceholder.typicode.com/posts"; + List _posts = new List(); + + using (var client = new HttpClient()) + { + var result = await client.GetAsync(url); + + if (result.IsSuccessStatusCode) + { + var content = await result.Content.ReadAsStringAsync(); + var deserializedPosts = JsonSerializer.Deserialize>(content, _options); + + _posts = deserializedPosts; + } + } + + return _posts; + } + + public async Task AddPostAsync(Post post) + { + var url = "https://jsonplaceholder.typicode.com/posts"; + Post addedPost = null; + + using (var client = new HttpClient()) + { + var serializePost = JsonSerializer.Serialize(post); + var content = new StringContent(serializePost, Encoding.UTF8, "application/json"); + var result = await client.PostAsync(url, content); + + if (result.IsSuccessStatusCode) + { + var response = await result.Content.ReadAsStringAsync(); + addedPost = JsonSerializer.Deserialize(response); + } + } + + return addedPost; + } + + public async Task UpdatePostAsync(int postId, Post post) + { + var url = $"https://jsonplaceholder.typicode.com/posts/{postId}"; + Post updatedPost = null; + + using (var client = new HttpClient()) + { + var serializePost = JsonSerializer.Serialize(post); + var content = new StringContent(serializePost, Encoding.UTF8, "application/json"); + var result = await client.PutAsync(url, content); + + if (result.IsSuccessStatusCode) + { + var response = await result.Content.ReadAsStringAsync(); + updatedPost = JsonSerializer.Deserialize(response); + } + } + + return updatedPost; + } + + public async Task DeletePostAsync(int postId) + { + var url = $"https://jsonplaceholder.typicode.com/posts/{postId}"; + + using (var client = new HttpClient()) + { + await client.DeleteAsync(url); + } + } + + public async Task GetFirstCommentByPostIdAsync(int postId) + { + var url = $"https://jsonplaceholder.typicode.com/posts/{postId}/comments"; + + List _comments = new List(); + + using (var client = new HttpClient()) + { + var result = await client.GetAsync(url); + + if (result.IsSuccessStatusCode) + { + var content = await result.Content.ReadAsStringAsync(); + var deserializedPosts = JsonSerializer.Deserialize>(content, _options); + + _comments = deserializedPosts; + } + } + + return _comments[0]; + } + } +} +``` + +* In here, we've implemented `IPostAppService` methods by using [jsonplaceholder](https://jsonplaceholder.typicode.co) API. These endpoints provide us basic crud functionallity. + +* After the implemenation, we can start to create the user interface. + +#### Blazor UI + +* We can create **/Posts** page for listing, updating, deleting and creating our posts. So, create a razor page named `Posts.razor` under **Pages** folder in `*.Blazor` project. + +**Posts.razor** +```razor +@page "/Posts" +@using TelerikComponents.Posts +@using IconName = Telerik.Blazor.IconName +@inject IPostAppService PostAppService + +

Posts

+ + + + + + + + Update + Edit + Delete + Display Comment + Cancel + + + + Add Post + + + + @* Modal *@ + + + Comment + + + + + +

Email: @Comment.Email

+

+ Message: @Comment.Body +

+
+
+
+``` + +**Post.razor.cs** +```csharp +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Telerik.Blazor.Components; +using TelerikComponents.Posts; + +namespace TelerikComponents.Blazor.Pages +{ + public partial class Posts + { + private List GridData { get; set; } + private TelerikGrid Grid { get; set; } + private bool ModalVisible { get; set; } = false; + private Comment Comment { get; set; } + + public Posts() + { + Comment = new Comment(); + } + + protected override async Task OnInitializedAsync() + { + await LoadDataAsync(); + } + + private async Task LoadDataAsync() + { + GridData = await PostAppService.GetPostsAsync(); + } + + private async Task UpdateHandler(GridCommandEventArgs args) + { + var post = (Post) args.Item; + + await PostAppService.UpdatePostAsync(post.Id, post); + + var matchingPost = GridData.FirstOrDefault(x => x.Id == post.Id); + + if (matchingPost != null) + { + matchingPost.Body = post.Body; + matchingPost.Title = post.Title; + } + } + + private async Task DeleteHandler(GridCommandEventArgs args) + { + var post = (Post) args.Item; + + GridData.Remove(post); + } + + private async Task CreateHandler(GridCommandEventArgs args) + { + var post = (Post) args.Item; + + var addedPost = await PostAppService.AddPostAsync(post); + + GridData.Insert(0, addedPost); + } + + private async Task PostDetailAsync(GridCommandEventArgs args) + { + var post = (Post) args.Item; + + Comment = await PostAppService.GetFirstCommentByPostIdAsync(post.Id); + + ModalVisible = true; + } + } +} +``` + +* In here, we've used `TelerikGrid` component. + +* The `Telerik Grid` is a powerful component, which allows you to visualize and edit data via its table representation. It provides a variety of options about how to present and perform operations over the underlying data, such as paging, sorting, filtering and editing. + +* The Blazor UI Grid allows flexible customization of its items exposing rows, columns and edit templates for this purpose. + +### Final Result + +* After all of these steps, we can finally run our application. + * Run `*.HttpApi.Host` project for use the required endpoints, + * Run `*.Blazor` project for see the Blazor UI. + +* When we navigate to `Posts` route, we should see the following screenshot in this page. + +![final-result](./final-result.jpg) + +## Conclusion + +In this article, I've tried to explain how we can integrate [Telerik Blazor Component](https://www.telerik.com/blazor-ui) to our Blazor UI. ABP Framework designed as modular, so that it can work with any UI library/framework. \ No newline at end of file diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/automated-nuget-feed-setup.png b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/automated-nuget-feed-setup.png new file mode 100644 index 0000000000..236135b30f Binary files /dev/null and b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/automated-nuget-feed-setup.png differ diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/final-result.jpg b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/final-result.jpg new file mode 100644 index 0000000000..244711a962 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/final-result.jpg differ diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/sample-application.jpg b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/sample-application.jpg new file mode 100644 index 0000000000..27e9dbe9fc Binary files /dev/null and b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/sample-application.jpg differ diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/telerik-blazor-component-1.jpg b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/telerik-blazor-component-1.jpg new file mode 100644 index 0000000000..7a204d97f3 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/telerik-blazor-component-1.jpg differ diff --git a/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/telerik-progress-bar.gif b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/telerik-progress-bar.gif new file mode 100644 index 0000000000..e1eff85986 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-Telerik-Blazor-Component/telerik-progress-bar.gif differ diff --git a/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/POST.md b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/POST.md new file mode 100644 index 0000000000..02cef11a2e --- /dev/null +++ b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/POST.md @@ -0,0 +1,1585 @@ +# Using Angular Material Components With the ABP Framework + +## Introduction + +Angular Material library is a popular and well-known library in the community. We will implement Angular Material components with the ABP framework in this article. We will follow the Book Store tutorial which documented in [ABP Documentation](https://docs.abp.io/en/abp/latest/Tutorials/Part-1) + +> This article doesn't include server-side parts except the **Author With Books Form** +> section. Please follow the tutorial for server-side parts. + +## Installation + +Create a project with ABP CLI. Run following command in terminal: + +```bash + abp new Acme.BookStore.AngularMaterial -u angular -o AcmeBookStoreAngularMaterial +``` + +Add Angular Material package to the created project. Run this command in the terminal at the `angular` directory: + +``` + ng add @angular/material +``` + +## Book CRUD Actions + +> Please complete the following steps before starting this section: +> +> - Follow the server-side steps at [Web Application Development Tutorial - Part 1: Creating the Server Side](https://docs.abp.io/en/abp/latest/Tutorials/Part-1) with remembering our application name is **Acme.BookStore.AngularMaterial** +> - Follow the localization part of [Web Application Development Tutorial - Part 2: The Book List Page](https://docs.abp.io/en/abp/latest/Tutorials/Part-2?UI=NG&DB=EF#localization) +> - Run following command in terminal at `angular` directory  +> `abp generate-proxy` + +In this section, we will create the book list page, book create and update dialog using the Angular Material Modules. + +Add material modules to `SharedModule`'s imports and exports arrays which placed in `angular/src/app/shared/shared.module.ts`: + +```typescript +import { MatCardModule } from "@angular/material/card"; +import { MatTableModule } from "@angular/material/table"; +import { MatPaginatorModule } from "@angular/material/paginator"; +import { MatSortModule } from "@angular/material/sort"; +import { MatButtonModule } from "@angular/material/button"; + +@NgModule({ + imports: [ + CoreModule, + ThemeSharedModule, + ThemeBasicModule, + NgbDropdownModule, + NgxValidateCoreModule, + + MatCardModule, // added this line + MatTableModule, // added this line + MatPaginatorModule, // added this line + MatSortModule, // added this line + MatButtonModule, // added this line + ], + exports: [ + CoreModule, + ThemeSharedModule, + ThemeBasicModule, + NgbDropdownModule, + NgxValidateCoreModule, + + MatCardModule, // added this line + MatTableModule, // added this line + MatPaginatorModule, // added this line + MatSortModule, // added this line + MatButtonModule, // added this line + ], +}) +export class SharedModule {} +``` + +Run the following command in the terminal at the `angular` directory to create the book module: + +``` +yarn ng generate module book --module app --routing --route books +``` + +Remove `CommonModule` form and import `SharedModule` from imports array at `book.module.ts`: + +```typescript +import { NgModule } from "@angular/core"; +import { BookRoutingModule } from "./book-routing.module"; +import { BookComponent } from "./book.component"; +import { SharedModule } from "../shared/shared.module"; + +@NgModule({ + declarations: [BookComponent], + imports: [ + BookRoutingModule, + SharedModule, // this line added + ], +}) +export class BookModule {} +``` + +> We deleted the `CommonModule` because `CommonModule` in `CoreModule`'s exports array and `CoreModule` in `SharedModule`'s exports array. + +We will add routes by adding items to the return array of the `routesProvider` created when creating a project for adding navigation elements for books route. For more information, see the [`RoutesService` document](https://docs.abp.io/en/abp/latest/UI/Angular/Modifying-the-Menu#via-routesservice). + +Open the `src/app/route.provider.ts` file replace the `configureRoutes` function declaration as shown below: + +```typescript +import { eLayoutType, RoutesService } from "@abp/ng.core"; + +function configureRoutes(routes: RoutesService) { + return () => { + routes.add([ + { + path: "/", + name: "::Menu:Home", + iconClass: "fas fa-home", + order: 1, + layout: eLayoutType.application, + }, + { + path: "/book-store", + name: "::Menu:BookStore", + iconClass: "fas fa-book", + order: 2, + layout: eLayoutType.application, + }, + { + path: "/books", + name: "::Menu:Books", + parentName: "::Menu:BookStore", + layout: eLayoutType.application, + }, + ]); + }; +} +``` + +### Book List + +Replace `BookComponent` with the following code placed at `angular/src/book/book.component.ts` : + +```typescript +import { Component, OnInit } from "@angular/core"; +import { ListService, PagedResultDto } from "@abp/ng.core"; +import { BookDto, BookService } from "@proxy/books"; +import { PageEvent } from "@angular/material/paginator"; +import { Sort } from "@angular/material/sort"; + +@Component({ + selector: "app-book", + templateUrl: "./book.component.html", + styleUrls: ["./book.component.scss"], + providers: [ListService], +}) +export class BookComponent implements OnInit { + book = { items: [], totalCount: 0 } as PagedResultDto; + columns: string[] = ["name", "type", "price"]; + + constructor( + public readonly list: ListService, + private bookService: BookService + ) { + this.list.maxResultCount = 2; + } + + ngOnInit() { + const bookStreamCreator = (query) => this.bookService.getList(query); + + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); + } + + changePage(pageEvent: PageEvent) { + this.list.page = pageEvent.pageIndex; + } + + changeSort(sort: Sort) { + this.list.sortKey = sort.active; + this.list.sortOrder = sort.direction; + } +} +``` + +- We imported and injected the generated `BookService`. +- We used the [ListService](https://docs.abp.io/en/abp/latest/UI/Angular/List-Service), a utility service of the ABP Framework which provides easy pagination, sorting and searching. +- We set `this.list.maxResultCount` to 2 in the constructor, it can be changed programmatically for example changing value with the dropdown in the template + +Replace the `book.component.html` in the `angular/src/book/` with following code: + +```html + + + + {{ '::Menu:Books' | abpLocalization }} + + + + + + + + + + + + + + + +
+ {{'::Name' | abpLocalization}} + {{element.name}} + {{'::Type' | abpLocalization}} + + {{ '::Enum:BookType:' + element.type | abpLocalization }} +
+ +
+
+``` + +- We used the [Material Card](https://material.angular.io/components/card/overview) component as a container +- We used [Material Table](https://material.angular.io/components/table/overview) and we made `name` and `type` columns sortable. `changeSort` method executes when sorting change. +- We used the [Material Pagination](https://material.angular.io/components/paginator/overview). `changePage` method executes when the page changed + +![Book List](book-list.gif) + +### Book Create + +In this section, we will create `BookDialogComponent` and we will display this component via [`Material Dialog`](https://material.angular.io/components/dialog/overview). We will use also [Material Input](https://material.angular.io/components/input/overview), [Material Select](https://material.angular.io/components/select/overview), [Material DatePicker](https://material.angular.io/components/datepicker/overview) modules in this component for book form. + +Create a new component named `BookDialogComponent` in the `angular/src/book/components` folder with the following command: + +``` +yarn ng generate component book/components/BookDialog --module book +``` + +> We used --module option for declaring in the component to a specific module. + +Add Material modules to `SharedModule`'s imports and exports arrays: + +```typescript +import { MatDialogModule } from "@angular/material/dialog"; +import { MatDatepickerModule } from "@angular/material/datepicker"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatInputModule } from "@angular/material/input"; +import { MatSelectModule } from "@angular/material/select"; +import { MatIconModule } from "@angular/material/icon"; +import { MatNativeDateModule } from "@angular/material/core"; + +@NgModule({ + imports: [ + // other imports + MatDialogModule, + MatDatepickerModule, + MatNativeDateModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatIconModule, + ], + exports: [ + // other exports + MatDialogModule, + MatDatepickerModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatIconModule, + ], +}) +export class SharedModule {} +``` + +Replace `book-dialog.component.ts` in `angular/src/book/` with following code: + +```typescript +import { Component, Inject, OnInit } from "@angular/core"; +import { + MAT_DIALOG_DATA, + MAT_DIALOG_DEFAULT_OPTIONS, +} from "@angular/material/dialog"; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { bookTypeOptions } from "@proxy/books"; + +@Component({ + selector: "app-book-dialog", + templateUrl: "./book-dialog.component.html", + styleUrls: ["./book-dialog.component.scss"], + providers: [ + { + provide: MAT_DIALOG_DEFAULT_OPTIONS, + useValue: { hasBackdrop: true, width: "50vw" }, + }, + ], +}) +export class BookDialogComponent implements OnInit { + form: FormGroup; + + bookTypes = bookTypeOptions; + + constructor(private fb: FormBuilder) {} + + ngOnInit(): void { + this.buildForm(); + } + + buildForm() { + this.form = this.fb.group({ + name: [null, Validators.required], + type: [null, Validators.required], + publishDate: [null, Validators.required], + price: [null, Validators.required], + }); + } + + getFormValue() { + const { publishDate } = this.form.value; + return { + ...this.form.value, + publishDate: `${publishDate?.getFullYear()}-${ + publishDate?.getMonth() + 1 + }-${publishDate?.getDate()}`, + }; + } +} +``` + +- We made a form which form controls' names are the same as BookDto +- We provided the `MAT_DIALOG_DEFAULT_OPTIONS` token to change Material Dialog options for this component. Provided options are only available for this component. + +Replace the `book-dialog.component.html` with following code: + +```html +

{{ '::NewBook' | abpLocalization }}

+ +
+ + {{'::Name' | abpLocalization}} * + + + + + {{'::Price' | abpLocalization}} * + + + + + {{'::Type' | abpLocalization}} * + + {{ '::Enum:BookType:' + type.value | abpLocalization }} + + + + + {{'::PublishDate' | abpLocalization}} * + + + + +
+
+ + + + +``` + +- We created a form with material form field components. +- We added 2 buttons for closing dialog and saving form in the `mat-dialog-actions` element. + +Create the `createBook` method and inject `MatDialog` in `book.component.ts`. Then use the material dialog's `open` method inside the `createBook` method: + +```typescript +import { BookDialogComponent } from "./components/book-dialog"; + +export class BookComponent { + constructor( + // ... + // inject dialog + public dialog: MatDialog + ) { + //... + } + //... other methods + createBook() { + const dialogRef = this.dialog.open(BookDialogComponent); + dialogRef.afterClosed().subscribe((result) => { + if (result) { + this.bookService.create(result).subscribe(() => { + this.list.get(); + }); + } + }); + } +} +``` + +- We displayed BookDialogComponent via Material Dialog. If the result has data after the dialog closes we made 2 HTTP requests for creating a book and refreshing the book list. + +Add create book button near the `mat-card-title` element in `book.component.html`: + +```html +{{ '::Menu:Books' | abpLocalization }} + +``` + +The final UI looks like below: + +![Book Create](./book-create.gif) + +### Edit Book + +We will use the same dialog component for editing the book. And we will add the `actions` column to the book list table. The actions column is a simple dropdown. We will use [Material Menu](https://material.angular.io/components/menu/overview) for creating dropdown + +Add `MatMenuModule` to `SharedModule` metadata's imports and exports array like this: + +```typescript +import { MatMenuModule } from "@angular/material/menu"; + +@NgModule({ + imports: [ + // other imports + MatMenuModule, + ], + exports: [ + // other exports + MatMenuModule, + ], +}) +export class SharedModule {} +``` + +Edit `columns` array and add `editBook` method in `book.component.ts` as shown below: + +```typescript +columns: string[] = ['actions', /* ... other columns*/]; + +editBook(id: string) { + this.bookService.get(id).subscribe((book) => { + const dialogRef = this.dialog.open(BookDialogComponent, { + data: book + }); + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.bookService.update(id, result).subscribe(() => { + this.list.get(); + }); + } + }); + }); +} +``` + +- We passed the data to `BookDialogComponent` with passing the data property `open` method of the material dialog. +- We checked data after closing the dialog for sending HTTP requests as in the `Create Book` section. + +Add actions column before name column and add `mat-menu` end of file in the `book.component.html` as shown below: + +```html + + + + {{'::Actions' | abpLocalization}} + + + + + + + + + + + + + + +``` + +Get passed data which passed with material dialog's `open` method and use this data to create a form with initial values in `book-dialog.component.ts` as shown below: + +```typescript + constructor( + //inject data + @Inject(MAT_DIALOG_DATA) public data: BookDto, + ) { + } + + buildForm() { + this.form = this.fb.group({ + name: [this.data?.name /*modified*/, Validators.required], + type: [this.data?.type /*modified*/, Validators.required], + publishDate: [this.data?.publishDate ? new Date(this.data.publishDate) : null, /*modified*/, Validators.required], + price: [this.data?.price /*modified*/, Validators.required], + }); + } +``` + +Edit the dialog title if component has data, display **Edit Book** text otherwise **New Book** in `book-dialog.component.html`: + +```html +

+ {{ (data ? '::EditBook' : '::NewBook' )| abpLocalization }} +

+``` + +![Book Edit](book-edit.gif) + +### Delete Book + +In the ABP Framework, a confirmation popup displays when the delete button is clicked. We will create a Confirmation Dialog and display this dialog with Material Dialog. + +Create `ConfirmationDialogComponent` in `angular/src/shared/components` directory with following command: + +``` +yarn ng generate component shared/components/ConfirmationDialog --module shared +``` + +Replace `ConfirmationDialogComponent` with following code: + +```typescript +import { Component, Inject } from "@angular/core"; +import { + MAT_DIALOG_DATA, + MAT_DIALOG_DEFAULT_OPTIONS, +} from "@angular/material/dialog"; + +export interface ConfirmationDialogData { + title: string; + description: string; +} +@Component({ + selector: "app-confirmation-dialog", + templateUrl: "./confirmation-dialog.component.html", + styleUrls: ["./confirmation-dialog.component.scss"], + providers: [ + { + provide: MAT_DIALOG_DEFAULT_OPTIONS, + useValue: { hasBackdrop: true, width: "450px" }, + }, + ], +}) +export class ConfirmationDialogComponent { + constructor(@Inject(MAT_DIALOG_DATA) public data: ConfirmationDialogData) {} +} +``` + +Replace `confirmation-dialog.component.html` with the following code: + +```html + +
+ warning +

{{ data.title | abpLocalization }}

+

{{ data.description | abpLocalization }}

+
+
+ + + + +``` + +Replace `confirmation-dialog.component.scss` with following code: + +```scss +:host { + .dialog-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + mat-icon.warn-icon { + font-size: 100px; + height: 100px; + width: 100px; + line-height: 100px; + } + p { + color: #777; + font-size: 16px; + } +} +``` + +Add `deleteBook` method to `BookComponent`: + +```typescript +import { BookDialogComponent } from './components/book-dialog/book-dialog.component'; +import { ConfirmationDialogComponent } from '../shared/components/confirmation-dialog/confirmation-dialog.component'; + +deleteBook(id: string) { + const confirmationDialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { + title: '::AreYouSure', + description: '::AreYouSureToDelete' + } + }); + confirmationDialogRef.afterClosed().subscribe(confirmationResult => { + if (confirmationResult) { + this.bookService.delete(id).subscribe(() => this.list.get()); + } + }); +} +``` + +Add delete button to actions' button menu template in `book.component.html`: + +```html + + + + + + + + +``` + +![Book Delete](./book-delete.gif) + +## Authorization + +You can follow steps for authorization at the [Web Application Development Tutorial - Part 5: Authorization](https://docs.abp.io/en/abp/latest/Tutorials/Part-5?UI=NG&DB=EF) + +## Author CRUD Actions + +> Please complete the following steps before starting this section +> +> - [Web Application Development Tutorial - Part 6: Authors: Domain Layer](https://docs.abp.io/en/abp/latest/Tutorials/Part-6) +> - [Web Application Development Tutorial - Part 7: Authors: Database Integration](https://docs.abp.io/en/abp/latest/Tutorials/Part-7) +> - [Web Application Development Tutorial - Part 8: Authors: Application Layer](https://docs.abp.io/en/abp/latest/Tutorials/Part-8) +> - Run following command in terminal at `angular` directory: +> `abp generate-proxy` + +In this section, we will create an author list page and author create/update dialog by following the same steps in the `Book CRUD Actions` section + +Run the following command in the terminal for creating author module and components: + +``` +yarn ng generate module author --module app --routing --route authors +``` + +Create `AuthorDialogComponent` in `angular/src/app/author/components` directory with following command: + +``` +yarn ng generate component author/components/AuthorDialog -m author +``` + +Add `SharedModule` to `AuthorModule`'s imports array: + +```typescript +import { NgModule } from "@angular/core"; +import { AuthorWithBooksRoutingModule } from "./author-with-books-routing.module"; +import { AuthorWithBooksComponent } from "./author-with-books.component"; +import { SharedModule } from "../shared/shared.module"; + +@NgModule({ + declarations: [AuthorComponent, AuthorDialogComponent], + imports: [SharedModule, AuthorRoutingModule], +}) +export class AuthorModule {} +``` + +Open the `src/app/route.provider.ts` file replace the `configureRoutes` function declaration as shown below: + +```typescript +function configureRoutes(routes: RoutesService) { + return () => { + routes.add([ + // other routes + { + path: "/authors", + name: "::Menu:Authors", + parentName: "::Menu:BookStore", + layout: eLayoutType.application, + }, + ]); + }; +} +``` + +Replace `AuthorDialogComponent` with following code below: + +```typescript +import { Component, Inject, OnInit } from "@angular/core"; +import { + MAT_DIALOG_DATA, + MAT_DIALOG_DEFAULT_OPTIONS, +} from "@angular/material/dialog"; +import { AuthorDto } from "@proxy/authors"; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; + +@Component({ + selector: "app-author-dialog", + templateUrl: "./author-dialog.component.html", + styleUrls: ["./author-dialog.component.scss"], + providers: [ + { + provide: MAT_DIALOG_DEFAULT_OPTIONS, + useValue: { hasBackdrop: true, width: "50vw" }, + }, + ], +}) +export class AuthorDialogComponent implements OnInit { + form: FormGroup; + + constructor( + @Inject(MAT_DIALOG_DATA) public data: AuthorDto, + private fb: FormBuilder + ) {} + ngOnInit(): void { + this.buildForm(); + } + + buildForm() { + this.form = this.fb.group({ + name: [this.data?.name, Validators.required], + birthDate: [ + this.data?.birthDate ? new Date(this.data.birthDate) : null, + Validators.required, + ], + }); + } + + getFormValue() { + const { birthDate } = this.form.value; + return { + ...this.form.value, + publishDate: `${birthDate?.getFullYear()}-${ + birthDate?.getMonth() + 1 + }-${birthDate?.getDate()}`, + }; + } +} +``` + +Replace `author-dialog.component.html` with the following code below: + +```html +

+ {{ (data ? '::EditAuthor' : '::NewAuthor' )| abpLocalization }} +

+ +
+ + {{'::Name' | abpLocalization}} * + + + + + {{'::BirthDate' | abpLocalization}} * + + + + +
+
+ + + + +``` + +Replace `author.component.ts` with following code below: + +```typescript +import { Component, OnInit } from "@angular/core"; +import { ListService, PagedResultDto } from "@abp/ng.core"; +import { AuthorDto, AuthorService } from "@proxy/authors"; +import { FormGroup } from "@angular/forms"; +import { PageEvent } from "@angular/material/paginator"; +import { Sort } from "@angular/material/sort"; +import { MatDialog } from "@angular/material/dialog"; +import { AuthorDialogComponent } from "./components/author-dialog/author-dialog.component"; +import { ConfirmationDialogComponent } from "../shared/components/confirmation-dialog/confirmation-dialog.component"; + +@Component({ + selector: "app-author", + templateUrl: "./author.component.html", + styleUrls: ["./author.component.scss"], + providers: [ListService], +}) +export class AuthorComponent implements OnInit { + author = { items: [], totalCount: 0 } as PagedResultDto; + + form: FormGroup; + + columns = ["actions", "name", "birthDate"]; + + constructor( + public readonly list: ListService, + private authorService: AuthorService, + public dialog: MatDialog + ) {} + + ngOnInit(): void { + const authorStreamCreator = (query) => this.authorService.getList(query); + + this.list.hookToQuery(authorStreamCreator).subscribe((response) => { + this.author = response; + }); + } + + changePage(pageEvent: PageEvent) { + this.list.page = pageEvent.pageIndex; + } + + changeSort(sort: Sort) { + this.list.sortKey = sort.active; + this.list.sortOrder = sort.direction; + } + + createAuthor() { + const dialogRef = this.dialog.open(AuthorDialogComponent); + dialogRef.afterClosed().subscribe((result) => { + if (result) { + this.authorService.create(result).subscribe(() => { + this.list.get(); + }); + } + }); + } + + editAuthor(id: any) { + this.authorService.get(id).subscribe((author) => { + const dialogRef = this.dialog.open(AuthorDialogComponent, { + data: author, + }); + dialogRef.afterClosed().subscribe((result) => { + if (result) { + this.authorService.update(id, result).subscribe(() => { + this.list.get(); + }); + } + }); + }); + } + + deleteAuthor(id: string) { + const confirmationDialogRef = this.dialog.open( + ConfirmationDialogComponent, + { + data: { + title: "::AreYouSure", + description: "::AreYouSureToDelete", + }, + } + ); + confirmationDialogRef.afterClosed().subscribe((confirmationResult) => { + if (confirmationResult) { + this.authorService.delete(id).subscribe(() => this.list.get()); + } + }); + } +} +``` + +Replace `author.component.html` with the following code: + +```html + + + + {{ '::Menu:Authors' | abpLocalization }} +
+ +
+
+
+ + + + + + + + + + + + + + +
+ {{'::Actions' | abpLocalization}} + + + + {{'::Name' | abpLocalization}} + {{element.name}} + {{'::BirthDate' | abpLocalization}} + + {{ element.birthDate | date }} +
+ +
+
+ + + + + + +``` + +Open the `en.json` file under the `Localization/BookStore` folder of the `Acme.BookStore.AngularMaterial.Domain.Shared project` and add the following entries: + +```json + "Menu:Authors": "Authors", + "Authors": "Authors", + "AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?", + "BirthDate": "Birth date", + "NewAuthor": "New author", + "EditAuthor": "Edit Author" +``` + +![Author](./author-crud.gif) + +## Author And Book Relation + +> Please complete the following steps before starting this section +> +> - Complete server-side parts [Web Application Development Tutorial - Part 10: Book to Author Relation](https://docs.abp.io/en/abp/latest/Tutorials/Part-10) until [The User Interface](https://docs.abp.io/en/abp/latest/Tutorials/Part-10?UI=NG&DB=EF#the-user-interface) section +> - Run following command in terminal at `angular` directory: +> `abp generate-proxy` + +In this section, we will add author selection to the book creation form, create one form for adding an author with books using Material Stepper and display the author's name in the book list page. + +### Author Selection + +We will add the author select box using Material Select and we will get authors from the server in `BookDialogComponent`. + +Replace `book-dialog.component.ts` in `app/src/book/components/book-dialog` with following code: + +```typescript +import { Component, Inject, OnInit } from "@angular/core"; +import { + MAT_DIALOG_DATA, + MAT_DIALOG_DEFAULT_OPTIONS, +} from "@angular/material/dialog"; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { + AuthorLookupDto, + BookDto, + BookService, + bookTypeOptions, +} from "@proxy/books"; +import { Observable } from "rxjs"; +import { map } from "rxjs/operators"; + +@Component({ + selector: "app-book-dialog", + templateUrl: "./book-dialog.component.html", + styleUrls: ["./book-dialog.component.scss"], + providers: [ + { + provide: MAT_DIALOG_DEFAULT_OPTIONS, + useValue: { hasBackdrop: true, width: "50vw" }, + }, + ], +}) +export class BookDialogComponent implements OnInit { + form: FormGroup; + + bookTypes = bookTypeOptions; + + authors$: Observable; // this line added + + constructor( + private fb: FormBuilder, + @Inject(MAT_DIALOG_DATA) public data: BookDto, + bookService: BookService // inject bookService + ) { + this.authors$ = bookService.getAuthorLookup().pipe(map((r) => r.items)); // this line added + } + + ngOnInit(): void { + this.buildForm(); + } + + buildForm() { + this.form = this.fb.group({ + name: [this.data?.name, Validators.required], + type: [this.data?.type, Validators.required], + publishDate: [this.data?.publishDate, Validators.required], + price: [this.data?.price, Validators.required], + authorId: [this.data?.authorId, Validators.required], // this line added + }); + } +} +``` + +Add author select box before name field in `book-dialog.component.html` as shown below: + +```html + + + {{'::Author' | abpLocalization}} * + + {{ author.name }} + + + + + {{'::Name' | abpLocalization}} * + + +``` + +### Author Name Column + +Add the `authorName` item to columns array in `BookComponent`: + +```typescript +columns: string[] = [/* ...other columns*/, 'authorName']; +``` + +Add the `authorName` column after price column in `book.component.html`: + +```html + + + + +``` + +### Author With Books Form + +In this section, we will create an endpoint that takes author information and book list in the request body for creating an author and books by one request. + +Create a class named `CreateBookDto` in `Application.Contracts/Books` folder: + +```csharp +using System; +using System.ComponentModel.DataAnnotations; +using Acme.BookStore.Books; +using Volo.Abp.Application.Dtos; + +namespace Acme.BookStore.AngularMaterial.Books +{ + public class CreateBookDto: AuditedEntityDto + { + [Required] + [StringLength(128)] + public string Name { get; set; } + + [Required] + public BookType Type { get; set; } = BookType.Undefined; + + [Required] + [DataType(DataType.Date)] + public DateTime PublishDate { get; set; } = DateTime.Now; + + [Required] + public float Price { get; set; } + } +} +``` + +Create a class named `CreateAuthorWithBookDto` in `Application.Contracts/Books` folder: + +```csharp +using System.Collections.Generic; +using Acme.BookStore.AngularMaterial.Authors; + +namespace Acme.BookStore.AngularMaterial.Books +{ + public class CreateAuthorWithBookDto: CreateAuthorDto + { + public List Books { get; set; } + + public CreateAuthorWithBookDto() + { + Books = new List(); + } + } +} +``` + +Create a class named `AuthorWithDetailsDto` in `Application.Contracts/Books` folder: + +```csharp +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Acme.BookStore.AngularMaterial.Authors; + +namespace Acme.BookStore.AngularMaterial.Books +{ + public class AuthorWithDetailsDto: AuthorDto + { + [Required] + public List Books { get; set; } + + public AuthorWithDetailsDto() + { + Books = new List(); + } + } +} +``` + +Add following line to `IBookAppService` interface which placed in `Application.Contracts/Books`: + +```csharp +Task CreateAuthorWithBooksAsync(CreateAuthorWithBookDto input); +``` + +Add mappings for the above DTO's lines to `AngularMaterialApplicationAutoMapperProfile.cs` + +```csharp +CreateMap(); +CreateMap(); +CreateMap(); +``` + +Inject `AuthorManager` and add `CreateAuthorWithBooksAsync` method to `BookAppService.cs`: + +```csharp +namespace Acme.BookStore.AngularMaterial.Books +{ + [Authorize(AngularMaterialPermissions.Books.Default)] + public class BookAppService: + CrudAppService< + Book, + BookDto, + Guid, + PagedAndSortedResultRequestDto, + CreateUpdateBookDto>, + IBookAppService + { + private readonly IAuthorRepository _authorRepository; + private readonly AuthorManager _authorManager; // this line added + public BookAppService( + IRepository repository, + IAuthorRepository authorRepository, + // inject AuthorManager + AuthorManager authorManager) + : base(repository) + { + _authorRepository = authorRepository; + _authorManager = authorManager;// this line added + GetPolicyName = AngularMaterialPermissions.Books.Default; + GetListPolicyName = AngularMaterialPermissions.Books.Default; + CreatePolicyName = AngularMaterialPermissions.Books.Create; + UpdatePolicyName = AngularMaterialPermissions.Books.Edit; + DeletePolicyName = AngularMaterialPermissions.Books.Delete; + } + + // Other class methods + + // This method added + public async Task CreateAuthorWithBooksAsync(CreateAuthorWithBookDto input) + { + var author = await _authorManager.CreateAsync( + input.Name, + input.BirthDate, + input.ShortBio + ); + + var createdAuthor = await _authorRepository.InsertAsync(author); + var authorWithBooks = ObjectMapper.Map(createdAuthor); + foreach (var book in input.Books) + { + var bookEntity = ObjectMapper.Map(book); + bookEntity.AuthorId = author.Id; + var createdBook = await Repository.InsertAsync(bookEntity); + var bookDto = ObjectMapper.Map(createdBook); + bookDto.AuthorName = author.Name; + authorWithBooks.Books.Add(bookDto); + } + + return authorWithBooks; + } + } +} +``` + +Open the `en.json` file under the `Localization/BookStore` folder of the `Acme.BookStore.AngularMaterial.Domain.Shared project` and add the following entries: + +```json + "AuthorInfo": "Author Info", + "BookInfo": "Book Info", + "AddBook": "Add Book", + "NewAuthorWithBook": "New Author With Book", + "AuthorWithBook:Success": "{0} added with books successfully" +``` + +Run generate-proxy command in the terminal at `angular` directory: + +``` +abp generate-proxy +``` + +We will create `AuthorWithBooksModule` with components and we will use Material Stepper inside the component. + +Run the following command for creating `AuthorWithBooksModule`: + +``` +yarn ng generate module author-with-books --module app --routing --route author-with-books +``` + +Add `SharedModule` to `AuthorWithBooksModule`'s imports array as shown below: + +```typescript +import { SharedModule } from "../shared/shared.module"; + +@NgModule({ + declarations: [AuthorWithBooksComponent], + imports: [ + SharedModule, // this line added + AuthorWithBooksRoutingModule, + ], +}) +export class AuthorWithBooksModule {} +``` + +Add `MatStepperModule` to `SharedModule`'s imports array as shown below: + +```typescript +import { MatStepperModule } from "@angular/material/stepper"; + +@NgModule({ + imports: [ + // other imports + MatStepperModule, + ], + exports: [ + // other exports + MatStepperModule, + ], +}) +export class SharedModule {} +``` + +We will create one form which includes author form group and book form array. We will use these form elements at the [Material Stepper](https://material.angular.io/components/stepper/overview) integration. + +Replace `author-with-books.component.ts` with the following code: + +```typescript +import { Component, OnInit } from "@angular/core"; +import { bookTypeOptions } from "@proxy/books"; +import { FormArray, FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { STEPPER_GLOBAL_OPTIONS } from "@angular/cdk/stepper"; + +@Component({ + selector: "app-author-with-book", + templateUrl: "./author-with-book.component.html", + styleUrls: ["./author-with-book.component.scss"], + providers: [ + { + provide: STEPPER_GLOBAL_OPTIONS, + useValue: { displayDefaultIndicatorType: false }, + }, + ], +}) +export class AuthorWithBooksComponent implements OnInit { + form: FormGroup; + + bookTypes = bookTypeOptions; + + get bookFormArray(): FormArray { + return this.form.get("books") as FormArray; + } + + constructor( + private fb: FormBuilder, + private bookService: BookService, + private toasterService: ToasterService + ) {} + + ngOnInit(): void { + this.form = this.fb.group({ + author: this.fb.group({ + name: [null, Validators.required], + birthDate: [null, Validators.required], + }), + books: this.fb.array([this.getBookForm()]), + }); + } + + getBookForm() { + return this.fb.group({ + name: [null, Validators.required], + type: [null, Validators.required], + publishDate: [null, Validators.required], + price: [null, Validators.required], + }); + } + + addBook() { + this.bookFormArray.push(this.getBookForm()); + } + + deleteBook(i: number) { + this.bookFormArray.removeAt(i); + } + + save() { + if (this.form.invalid) { + return; + } + const authorWithBook: CreateAuthorWithBookDto = { + ...this.form.value.author, + books: this.form.value.books, + }; + this.bookService.createAuthorWithBooks(authorWithBook).subscribe((res) => { + this.toasterService.success("::AuthorWithBook:Success", "", { + messageLocalizationParams: [res.name], + }); + }); + } +} +``` + +- We created form until component initialization in the `ngOnInit` method +- In the `getBookForm` method, returned +- In the `addBook` method, we pushed a form group instance created in the `getBookForm` method to the book form array. We will execute this method when clicked on the **Add Book** button +- We deleted a form group instance by index from the book form array in the `deleteBook` method +- In the `save` method, we send an HTTP request for creating author and books if creation will be successful toaster message will be display + +Replace the `author-with-books.component.html` content with following code: + +```html + + +
+ + {{'::Name' | abpLocalization}} * + + + + + {{'::BirthDate' | abpLocalization}} * + + + + +
+
+ + +
+ + {{'::Name' | abpLocalization}} * + + + + + {{'::Price' | abpLocalization}} * + + + + + {{'::Type' | abpLocalization}} * + + {{'::SelectBookType' | abpLocalization}} + {{ '::Enum:BookType:' + type.value | abpLocalization + }} + + + + + {{'::PublishDate' | abpLocalization}} * + + + + +
+ +
+
+ + +
+
+ + person + + + book + +
+``` + +- We created the same author form where we created in `AuthorDialogComponent` and we gave the author form group to mat-step's stepConrol input +- We created the same book form which we created at `BookDialogComponent` except the author selection + +Replace `author-with-books.component.scss` content with following code: + +```scss +.book-form { + margin-top: 20px; +} +.button-container { + display: flex; + width: 100%; + justify-content: space-between; + margin-top: 25px; +} +``` + +Finally add Create Author With Books button near the Create Author button in `author.component.html`: + +```html +
+ + +
+``` + +Final UI looks as shown below: + +![Author With Books](./author-with-books.gif) + +## The Source Code + +You can download the source code from [here](https://github.com/abpframework/abp-samples/tree/master/AcmeBookStoreAngularMaterial). + +## Conclusion + +We implemented Angular Material Components to our angular application which was created with ABP Framework. There is no blocker case of using angular libraries with the ABP framework. diff --git a/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/author-crud.gif b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/author-crud.gif new file mode 100644 index 0000000000..be7e2246ed Binary files /dev/null and b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/author-crud.gif differ diff --git a/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/author-with-books.gif b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/author-with-books.gif new file mode 100644 index 0000000000..698507f780 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/author-with-books.gif differ diff --git a/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/book-create.gif b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/book-create.gif new file mode 100644 index 0000000000..613ccf4a45 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/book-create.gif differ diff --git a/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/book-delete.gif b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/book-delete.gif new file mode 100644 index 0000000000..a10d8a3536 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/book-delete.gif differ diff --git a/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/book-edit.gif b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/book-edit.gif new file mode 100644 index 0000000000..387274aa38 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/book-edit.gif differ diff --git a/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/book-list.gif b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/book-list.gif new file mode 100644 index 0000000000..1a88413e40 Binary files /dev/null and b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/book-list.gif differ diff --git a/docs/en/Contribution/Index.md b/docs/en/Contribution/Index.md index 83ec0bf05d..d2525ae191 100644 --- a/docs/en/Contribution/Index.md +++ b/docs/en/Contribution/Index.md @@ -2,31 +2,36 @@ ABP is an [open source](https://github.com/abpframework) and community driven project. This guide is aims to help anyone wants to contribute to the project. -## community.abp.io +## ABP Community Website -If you want to write articles or "how to" guides related to the ABP Framework and ASP.NET Core, please submit your article to the [community.abp.io](https://community.abp.io/) web site. +If you want to write **articles** or **how to guides** related to the ABP Framework and ASP.NET Core, please submit your article to the [community.abp.io](https://community.abp.io/) website. ## Code Contribution -You can always send pull requests to the Github repository. +You can always send pull requests to the GitHub repository. -- Clone the [ABP repository](https://github.com/abpframework/abp/) from Github. -- Make the required changes. +- [Fork](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo) the [ABP repository](https://github.com/abpframework/abp/) from GitHub. +- Build the repository using the `/build/build-all.ps1 -f` for one time. +- Make the necessary changes, including unit/integration tests. - Send a pull request. +> When you open a solution in Visual Studio, you may need to execute `dotnet restore` in the root folder of the solution for one time, after it is fully opened in the Visual Studio. This is needed since VS can't properly resolves local references to projects out of the solution. + +### GitHub Issues + Before making any change, please discuss it on the [Github issues](https://github.com/abpframework/abp/issues). In this way, no other developer will work on the same issue and your PR will have a better chance to be accepted. -### Bug Fixes & Enhancements +#### Bug Fixes & Enhancements You may want to fix a known bug or work on a planned enhancement. See [the issue list](https://github.com/abpframework/abp/issues) on Github. -### Feature Requests +#### Feature Requests If you have a feature idea for the framework or modules, [create an issue](https://github.com/abpframework/abp/issues/new) on Github or attend to an existing discussion. Then you can implement it if it's embraced by the community. ## Document Translation -You may want to translate the complete [documentation](https://abp.io/documents/) (including this one) to your mother language. If so, follow these steps: +You may want to translate the complete [documentation](https://docs.abp.io) (including this one) to your mother language. If so, follow these steps: * Clone the [ABP repository](https://github.com/abpframework/abp/) from Github. * To add a new language, create a new folder inside the [docs](https://github.com/abpframework/abp/tree/master/docs) folder. Folder names can be "en", "es", "fr", "tr" and so on based on the language (see [all culture codes](https://msdn.microsoft.com/en-us/library/hh441729.aspx)). @@ -35,9 +40,9 @@ You may want to translate the complete [documentation](https://abp.io/documents/ There are some fundamental documents need to be translated before publishing a language on the [ABP documentation web site](https://docs.abp.io): -* Getting Started documents -* Tutorials -* CLI +* Index (Home) +* Getting Started +* Web Application Development Tutorial A new language is published after these minimum translations have been completed. diff --git a/docs/en/Customizing-Application-Modules-Guide.md b/docs/en/Customizing-Application-Modules-Guide.md index d184d82906..56b94ae4fc 100644 --- a/docs/en/Customizing-Application-Modules-Guide.md +++ b/docs/en/Customizing-Application-Modules-Guide.md @@ -43,17 +43,59 @@ In any case, you can create a **separate solution** for the desired module and d #### Publishing the Customized Module as Packages -One alternative scenario could be re-packaging the module source code (as NuGet/NPM packages) and using as package references. You can use a local private NuGet/NPM server for your company. +One alternative scenario could be re-packaging the module source code (as NuGet/NPM packages) and using as package references. You can use a local private NuGet/NPM server for your company, for example. ## Module Customization / Extending Approaches -This section suggests some approaches if you decided to use pre-built application modules as NuGet/NPM package references. The following documents explain how to customize/extend existing modules in different ways: +This section suggests some approaches if you decided to use pre-built application modules as NuGet/NPM package references. The following documents explain how to customize/extend existing modules in different ways. + +### Module Entity Extension System + +> Module entity extension system is the **main and high level extension system** that allows you to **define new properties** for existing entities of the depended modules. It automatically **adds properties to the entity, database, HTTP API and the user interface** in a single point. + +See the [Module Entity Extensions document](Module-Entity-Extensions.md) to learn how to use it. + +### Extending Entities + +If you only need to get/set extra data on an existing entity, follow the [Extending Entities](Customizing-Application-Modules-Extending-Entities.md) document. + +### Overriding Services/Components + +In addition to the extensibility systems, you can partially or completely override any service or user interface page/component. -* [Extending Entities](Customizing-Application-Modules-Extending-Entities.md) * [Overriding Services](Customizing-Application-Modules-Overriding-Services.md) * [Overriding the User Interface](Customizing-Application-Modules-Overriding-User-Interface.md) -### See Also +### Additional UI Extensibility Points + +There are some low level systems that you can control entity actions, table columns and page toolbar of a page defined by a module. + +#### Entity Actions + +Entity action extension system allows you to add a new action to the action menu for an entity on the user interface; + +* [Entity Action Extensions for ASP.NET Core UI](UI/AspNetCore/Entity-Action-Extensions.md) +* [Entity Action Extensions for Angular](UI/Angular/Entity-Action-Extensions.md) + +#### Data Table Column Extensions + +Data table column extension system allows you to add a new column in the data table on the user interface; + +* [Data Table Column Extensions for ASP.NET Core UI](UI/AspNetCore/Data-Table-Column-Extensions.md) +* [Data Table Column Extensions for Angular](UI/Angular/Data-Table-Column-Extensions.md) + +#### Page Toolbar + +Page toolbar system allows you to add components to the toolbar of a page; + +* [Page Toolbar Extensions for ASP.NET Core UI](UI/AspNetCore/Page-Toolbar-Extensions.md) +* [Page Toolbar Extensions for Angular](UI/Angular/Page-Toolbar-Extensions.md) + +#### Others + +* [Dynamic Form Extensions for Angular](UI/Angular/Dynamic-Form-Extensions.md) + +## See Also Also, see the following documents: diff --git a/docs/en/Customizing-Application-Modules-Overriding-Services.md b/docs/en/Customizing-Application-Modules-Overriding-Services.md index b8706eea61..ab6f29816b 100644 --- a/docs/en/Customizing-Application-Modules-Overriding-Services.md +++ b/docs/en/Customizing-Application-Modules-Overriding-Services.md @@ -29,7 +29,7 @@ public class TestAppService : IIdentityUserAppService, ITransientDependency } ```` -The dependency injection system allows to register multiple services for the same interface. The last registered one is used when the interface is injected. It is a good practice to explicitly replace the service. +The dependency injection system allows to register multiple services for the same interface. The last registered one is used when the interface is injected. It is a good practice to explicitly replace the service. Example: @@ -59,7 +59,6 @@ In most cases, you will want to change one or a few methods of the current imple ### Example: Overriding an Application Service ````csharp -//[RemoteService(IsEnabled = false)] // If you use dynamic controller feature you can disable remote service. Prevent creating duplicate controller for the application service. [Dependency(ReplaceServices = true)] [ExposeServices(typeof(IIdentityUserAppService), typeof(IdentityUserAppService), typeof(MyIdentityUserAppService))] public class MyIdentityUserAppService : IdentityUserAppService @@ -76,7 +75,7 @@ public class MyIdentityUserAppService : IdentityUserAppService { } - public override async Task CreateAsync(IdentityUserCreateDto input) + public async override Task CreateAsync(IdentityUserCreateDto input) { if (input.PhoneNumber.IsNullOrWhiteSpace()) { @@ -109,33 +108,33 @@ public class MyIdentityUserManager : IdentityUserManager { public MyIdentityUserManager( IdentityUserStore store, - IIdentityRoleRepository roleRepository, + IIdentityRoleRepository roleRepository, IIdentityUserRepository userRepository, - IOptions optionsAccessor, + IOptions optionsAccessor, IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, + IEnumerable> userValidators, + IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, - ILogger logger, - ICancellationTokenProvider cancellationTokenProvider) : + ILogger logger, + ICancellationTokenProvider cancellationTokenProvider) : base(store, roleRepository, - userRepository, - optionsAccessor, - passwordHasher, - userValidators, + userRepository, + optionsAccessor, + passwordHasher, + userValidators, passwordValidators, - keyNormalizer, - errors, - services, - logger, + keyNormalizer, + errors, + services, + logger, cancellationTokenProvider) { } - public override async Task CreateAsync(IdentityUser user) + public async override Task CreateAsync(IdentityUser user) { if (user.PhoneNumber.IsNullOrWhiteSpace()) { @@ -182,7 +181,7 @@ namespace MyProject.Controllers } - public override async Task SendPasswordResetCodeAsync( + public async override Task SendPasswordResetCodeAsync( SendPasswordResetCodeDto input) { Logger.LogInformation("Your custom logic..."); @@ -197,6 +196,15 @@ This example replaces the `AccountController` (An API Controller defined in the **`[ExposeServices(typeof(AccountController))]` is essential** here since it registers this controller for the `AccountController` in the dependency injection system. `[Dependency(ReplaceServices = true)]` is also recommended to clear the old registration (even the ASP.NET Core DI system selects the last registered one). +In addition, The `AccountController` will be removed from [`ApplicationModel`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.applicationmodels.applicationmodel.controllers) because it defines `ExposeServicesAttribute`. If you don't want to remove it, you can configure `AbpAspNetCoreMvcOptions`: + +```csharp +Configure(options => +{ + options.IgnoredControllersOnModelExclusion.AddIfNotContains(typeof(AccountController)); +}); +``` + ### Overriding Other Classes Overriding controllers, framework services, view component classes and any other type of classes registered to dependency injection can be overridden just like the examples above. @@ -214,7 +222,7 @@ Assuming that you've already added a `SocialSecurityNumber` as described in the You can use the [object extension system](Object-Extensions.md) to add the property to the `IdentityUserDto`. Write this code inside the `YourProjectNameDtoExtensions` class comes with the application startup template: ````csharp -ObjectExtensionManager.Instance +ObjectExtensionManager.Instance .AddOrUpdateProperty( "SocialSecurityNumber" ); @@ -286,8 +294,8 @@ ObjectExtensionManager.Instance .AddOrUpdateProperty( new[] { - typeof(IdentityUserDto), - typeof(IdentityUserCreateDto), + typeof(IdentityUserDto), + typeof(IdentityUserCreateDto), typeof(IdentityUserUpdateDto) }, "SocialSecurityNumber" diff --git a/docs/en/Data-Access.md b/docs/en/Data-Access.md index a37f77eb65..7ba85956b9 100644 --- a/docs/en/Data-Access.md +++ b/docs/en/Data-Access.md @@ -1,15 +1,13 @@ # Data Access -## Database Providers - -ABP framework was designed as database agnostic. It can work any type of data source by the help of the [repository](Repositories.md) and [unit of work](Unit-Of-Work.md) abstractions. However, currently the following providers are implemented: +ABP framework was designed as database agnostic. It can work any type of data source by the help of the [repository](Repositories.md) and [unit of work](Unit-Of-Work.md) abstractions. Currently, the following providers are implemented as official: * [Entity Framework Core](Entity-Framework-Core.md) (works with [various DBMS and providers](https://docs.microsoft.com/en-us/ef/core/providers/).) * [MongoDB](MongoDB.md) * [Dapper](Dapper.md) -More providers will be added in the future. - ## See Also -* [Connection Strings](Connection-Strings.md) \ No newline at end of file +* [Connection Strings](Connection-Strings.md) +* [Data Seeding](Data-Seeding.md) +* [Data Filtering](Data-Filtering.md) \ No newline at end of file diff --git a/docs/en/Data-Filtering.md b/docs/en/Data-Filtering.md index 626719b2fc..6df738cf18 100644 --- a/docs/en/Data-Filtering.md +++ b/docs/en/Data-Filtering.md @@ -32,6 +32,8 @@ namespace Acme.BookStore > `ISoftDelete` filter is enabled by default and you can not get deleted entities from database unless you explicitly disable it. See the `IDataFilter` service below. +> Soft-delete entities can be hard-deleted when you use `HardDeleteAsync` method on the repositories. + ### IMultiTenant [Multi-tenancy](Multi-Tenancy.md) is an efficient way of creating SaaS applications. Once you create a multi-tenant application, you typically want to isolate data between tenants. Implement `IMultiTenant` interface to make your entity "multi-tenant aware". diff --git a/docs/en/Data-Seeding.md b/docs/en/Data-Seeding.md index 8827fe96c0..f3e9414f0f 100644 --- a/docs/en/Data-Seeding.md +++ b/docs/en/Data-Seeding.md @@ -38,31 +38,37 @@ namespace Acme.BookStore { private readonly IRepository _bookRepository; private readonly IGuidGenerator _guidGenerator; + private readonly ICurrentTenant _currentTenant; public BookStoreDataSeedContributor( IRepository bookRepository, - IGuidGenerator guidGenerator) + IGuidGenerator guidGenerator, + ICurrentTenant currentTenant) { _bookRepository = bookRepository; _guidGenerator = guidGenerator; + _currentTenant = currentTenant; } public async Task SeedAsync(DataSeedContext context) { - if (await _bookRepository.GetCountAsync() > 0) + using (_currentTenant.Change(context?.TenantId)) { - return; + if (await _bookRepository.GetCountAsync() > 0) + { + return; + } + + var book = new Book( + id: _guidGenerator.Create(), + name: "The Hitchhiker's Guide to the Galaxy", + type: BookType.ScienceFiction, + publishDate: new DateTime(1979, 10, 12), + price: 42 + ); + + await _bookRepository.InsertAsync(book); } - - var book = new Book( - id: _guidGenerator.Create(), - name: "The Hitchhiker's Guide to the Galaxy", - type: BookType.ScienceFiction, - publishDate: new DateTime(1979, 10, 12), - price: 42 - ); - - await _bookRepository.InsertAsync(book); } } } diff --git a/docs/en/Domain-Driven-Design-Implementation-Guide.md b/docs/en/Domain-Driven-Design-Implementation-Guide.md new file mode 100644 index 0000000000..9341df703b --- /dev/null +++ b/docs/en/Domain-Driven-Design-Implementation-Guide.md @@ -0,0 +1,1978 @@ +# Implementing Domain Driven Design + +## Introduction + +This is a **practical guide** for implementing the Domain Driven Design (DDD). While the implementation details rely on the ABP Framework infrastructure, core concepts, principles and patterns are applicable in any kind of solution, even if it is not a .NET solution. + +### Goals + +The goals of this document are to + +* **Introduce and explain** the DDD architecture, concepts, principles, patterns and building blocks. +* Explain the **layered architecture** & solution structure offered by the ABP Framework. +* Introduce **explicit rules** to implement DDD patterns and best practices by giving **concrete examples**. +* Show what **ABP Framework provides** you as the infrastructure for implementing DDD in a proper way. +* And finally, provide **suggestions** based on software development **best practices** and our experiences to create a **maintainable codebase**. + +### Simple Code! + +> **Playing football** is very **simple**, but **playing simple football** is the **hardest thing** there is. +> — Johan Cruyff + +If we take this famous quote for programming, we can say; + +> **Writing code** is very **simple**, but **writing simple code** is the **hardest thing** there is. +> — ??? + +In this document, we will introduce **simple rules**, those are **easy to implement**. + +Once your **application grows**, it will be **hard to follow** these rules. Sometimes you find **breaking rules** will save you time in a short term. However, the saved time in the short term will bring much **more time loss** in the middle and long term. Your code base becomes **complicated** and hard to maintain. Most of the business applications are **re-written** just because you **can't maintain** it anymore. + +If you **follow the rules and best practices**, your code base will be simpler and easier to maintain. Your application **reacts to changes** faster. + +## What is the Domain Driven Design? + +Domain-driven design (DDD) is an approach to software development for **complex** needs by connecting the implementation to an **evolving** model; + +DDD is suitable for **complex domains** and **large-scale** applications rather than simple CRUD applications. It focuses on the **core domain logic** rather than the infrastructure details. It helps to build a **flexible**, modular and **maintainable** code base. + +### OOP & SOLID + +Implementing DDD highly relies on the Object Oriented Programming (OOP) and [SOLID](https://en.wikipedia.org/wiki/SOLID) principles. Actually, it **implements** and **extends** these principles. So, a **good understanding** of OOP & SOLID helps you a lot while truly implementing the DDD. + +### DDD Layers & Clean Architecture + +There are four fundamental layers of a Domain Driven Based Solution; + +![domain-driven-design-layers](images/domain-driven-design-layers.png) + +**Business Logic** places into two layers, the *Domain layer* and the *Application Layer*, while they contain different kinds of business logic; + +* **Domain Layer** implements the core, use-case independent business logic of the domain/system. +* **Application Layer** implements the use cases of the application based on the domain. A use case can be thought as a user interaction on the User Interface (UI). +* **Presentation Layer** contains the UI elements (pages, components) of the application. +* **Infrastructure Layer** supports other layer by implementing the abstractions and integrations to 3rd-party library and systems. + +The same layering can be shown as the diagram below and known as the **Clean Architecture**, or sometimes the **Onion Architecture**: + +![domain-driven-design-clean-architecture](images/domain-driven-design-clean-architecture.png) + +In the Clean Architecture, each layer only **depends on the layer directly inside it**. The most independent layer is shown in the most inner circle and it is the Domain Layer. + +### Core Building Blocks + +DDD mostly **focuses on the Domain & Application Layers** and ignores the Presentation and Infrastructure. They are seen as *details* and the business layers should not depend on them. + +That doesn't mean the Presentation and Infrastructure layers are not important. They are very important. UI frameworks and database providers have their own rules and best practices that you need to know and apply. However these are not in the topics of DDD. + +This section introduces the essential building blocks of the Domain & Application Layers. + +#### Domain Layer Building Blocks + +* **Entity**: An [Entity](Entities.md) is an object with its own properties (state, data) and methods that implements the business logic that is executed on these properties. An entity is represented by its unique identifier (Id). Two entity object with different Ids are considered as different entities. +* **Value Object**: A [Value Object](Value-Objects.md) is another kind of domain object that is identified by its properties rather than a unique Id. That means two Value Objects with same properties are considered as the same object. Value objects are generally implemented as immutable and mostly are much simpler than the Entities. +* **Aggregate & Aggregate Root**: An [Aggregate](Entities.md) is a cluster of objects (entities and value objects) bound together by an **Aggregate Root** object. The Aggregate Root is a specific type of an entity with some additional responsibilities. +* **Repository** (interface): A [Repository](Repositories.md) is a collection-like interface that is used by the Domain and Application Layers to access to the data persistence system (the database). It hides the complexity of the DBMS from the business code. Domain Layer contains the `interface`s of the repositories. +* **Domain Service**: A [Domain Service](Domain-Services.md) is a stateless service that implements core business rules of the domain. It is useful to implement domain logic that depends on multiple aggregate (entity) type or some external services. +* **Specification**: A [Specification](Specifications.md) is used to define named, reusable and combinable filters for entities and other business objects. +* **Domain Event**: A [Domain Event](Event-Bus.md) is a way of informing other services in a loosely coupled manner, when a domain specific event occurs. + +#### Application Layer Building Blocks + +* **Application Service**: An [Application Service](Application-Services.md) is a stateless service that implements use cases of the application. An application service typically gets and returns DTOs. It is used by the Presentation Layer. It uses and coordinates the domain objects to implement the use cases. A use case is typically considered as a Unit Of Work. +* **Data Transfer Object (DTO)**: A [DTO](Data-Transfer-Objects.md) is a simple object without any business logic that is used to transfer state (data) between the Application and Presentation Layers. +* **Unit of Work (UOW)**: A [Unit of Work](Unit-Of-Work.md) is an atomic work that should be done as a transaction unit. All the operations inside a UOW should be committed on success or rolled back on a failure. + +## Implementation: The Big Picture + +### Layering of a .NET Solution + +The picture below shows a Visual Studio Solution created using the ABP's [application startup template](Startup-Templates/Application.md): + +![domain-driven-design-vs-solution](images/domain-driven-design-vs-solution.png) + +The solution name is `IssueTracking` and it consists of multiple projects. The solution is layered by considering **DDD principles** as well as **development** and **deployment** practicals. The sub sections below explains the projects in the solution; + +> Your solution structure may be slightly different if you choose a different UI or Database provider. However, the Domain and Application layers will be same and this is the essential point for the DDD perspective. See the [Application Startup Template](Startup-Templates/Application.md) document if you want to know more about the solution structure. + +#### The Domain Layer + +The Domain Layer is splitted into two projects; + +* `IssueTracking.Domain` is the **essential domain layer** that contains all the **building blocks** (entities, value objects, domain services, specifications, repository interfaces, etc.) introduced before. +* `IssueTracking.Domain.Shared` is a thin project that contains some types those belong to the Domain Layer, but shared with all other layers. For example, it may contain some constants and `enum`s related to the Domain Objects but need to be **reused by other layers**. + +#### The Application Layer + +The Application Layer is also splitted into two projects; + +* `IssueTracking.Application.Contracts` contains the application service **interfaces** and the **DTO**s used by these interfaces. This project can be shared by the client applications (including the UI). +* `IssueTracking.Application` is the **essential application layer** that **implements** the interfaces defined in the Contracts project. + +#### The Presentation Layer + +* `IssueTracking.Web` is an ASP.NET Core MVC / Razor Pages application for this example. This is the only executable application that serves the application and the APIs. + +> ABP Framework also supports different kind of UI frameworks including [Angular](UI/Angular/Quick-Start.md) and [Blazor](UI/Blazor/Overall.md). In these cases, the `IssueTracking.Web` doesn't exist in the solution. Instead, an `IssueTracking.HttpApi.Host` application will be in the solution to serve the HTTP APIs as a standalone endpoint to be consumed by the UI applications via HTTP API calls. + +#### The Remote Service Layer + +* `IssueTracking.HttpApi` project contains HTTP APIs defined by the solution. It typically contains MVC `Controller`s and related models, if available. So, you write your HTTP APIs in this project. + +> Most of the time, API Controllers are just wrappers around the Application Services to expose them to the remote clients. Since ABP Framework's [Automatic API Controller System](API/Auto-API-Controllers.md) **automatically configures and exposes your Application Services as API Controllers**, you typically don't create Controllers in this project. However, the startup solution includes it for the cases you need to manually create API controllers. + +* `IssueTracking.HttpApi.Client` project is useful when you have a C# application that needs to consume your HTTP APIs. Once the client application references this project, it can directly [inject](Dependency-Injection.md) & use the Application Services. This is possible by the help of the ABP Framework's [Dynamic C# Client API Proxies System](API/Dynamic-CSharp-API-Clients.md). + +> There is a Console Application in the `test` folder of the solution, named `IssueTracking.HttpApi.Client.ConsoleTestApp`. It simply uses the `IssueTracking.HttpApi.Client` project to consume the APIs exposed by the application. It is just a demo application and you can safely delete it. You can even delete the `IssueTracking.HttpApi.Client` project if you think that you don't need to them. + +#### The Infrastructure Layer + +In a DDD implementation, you may have a single Infrastructure project to implement all the abstractions and integrations, or you may have different projects for each dependency. + +We suggest a balanced approach; Create separate projects for main infrastructure dependencies (like Entity Framework Core) and a common infrastructure project for other infrastructure. + +ABP's startup solution has two projects for the Entity Framework Core integration; + +* `IssueTracking.EntityFrameworkCore` is the essential integration package for the EF Core. Your application's `DbContext`, database mappings, implementations of the repositories and other EF Core related stuff are located here. +* `IssueTracking.EntityFrameworkCore.DbMigrations` is a special project to manage the Code First database migrations. There is a separate `DbContext` in this project to track the migrations. You typically don't touch this project much except you need to create a new database migration or add an [application module](Modules/Index.md) that has some database tables and naturally requires to create a new database migration. + +> You may wonder why there are two projects for the EF Core. It is mostly related to [modularity](Module-Development-Basics.md). Each module has its own independent `DbContext` and your application has also one `DbContext`. `DbMigrations` project contains a **union** of the modules to track and apply a **single migration path**. While most of the time you don't need to know it, you can see the [EF Core migrations](Entity-Framework-Core-Migrations.md) document for more information. + +#### Other Projects + +There is one more project, `IssueTracking.DbMigrator`, that is a simple Console Application that **migrates** the database schema and **[seeds](Data-Seeding.md) the initial** data when you execute it. It is a useful **utility application** that you can use it in development as well as in production environment. + +### Dependencies of the Projects in the Solution + +The diagram below shows the essential dependencies (project references) between the projects in the solution (`IssueTracking.` part is not shown to be simple) + +![domain-driven-design-project-relations](images/domain-driven-design-project-relations.png) + +The projects have been explained before. Now, we can explain the reasons of the dependencies; + +* `Domain.Shared` is the project that all other projects directly or indirectly depend on. So, all the types in this project are available to all projects. +* `Domain` only depends on the `Domain.Shared` because it is already a (shared) part of the domain. For example, an `IssueType` enum in the `Domain.Shared` can be used by an `Issue` entity in the `Domain` project. +* `Application.Contracts` depends on the `Domain.Shared`. In this way, you can reuse these types in the DTOs. For example, the same `IssueType` enum in the `Domain.Shared` can be used by a `CreateIssueDto` as a property. +* `Application` depends on the `Application.Contracts` since it implements the Application Service interfaces and uses the DTOs inside it. It also depends on the `Domain` since the Application Services are implemented using the Domain Objects defined inside it. +* `EntityFrameworkCore` depends on the `Domain` since it maps the Domain Objects (entities and value types) to database tables (as it is an ORM) and implements the repository interfaces defined in the `Domain`. +* `HttpApi` depends on the `Application.Contacts` since the Controllers inside it inject and use the Application Service interfaces as explained before. +* `HttpApi.Client` depends on the `Application.Contacts` since it can consume the Application Services as explained before. +* `Web` depends on the `HttpApi` since it serves the HTTP APIs defined inside it. Also, in this way, it indirectly depends on the `Application.Contacts` project to consume the Application Services in the Pages/Components. + +#### Dashed Dependencies + +When you investigate the solution, you will see two more dependencies shown with the dashed lines in the figure above. `Web` project depends on the `Application` and `EntityFrameworkCore` projects which *theoretically* should not be like that but actually it is. + +This is because the `Web` is the final project that runs and hosts the application and the **application needs the implementations of the Application Services and the Repositories** while running. + +This design decision potentially allows you to use Entities and EF Core objects in the Presentation Layer which **should be strictly avoided**. However, we find the alternative designs over complicated. Here, two of the alternatives if you want to remove this dependency; + +* Convert `Web` project to a razor class library and create a new project, like `Web.Host`, that depends on the `Web`, `Application` and `EntityFrameworkCore` projects and hosts the application. You don't write any UI code here, but use **only for hosting**. +* Remove `Application` and `EntityFrameworkCore` dependencies from the `Web` project and load their assemblies on application initialization. You can use ABP's [Plug-In Modules](PlugIn-Modules.md) system for that purpose. + +### Execution Flow a DDD Based Application + +The figure below shows a typical request flow for a web application that has been developed based on DDD patterns. + +![](images/domain-driven-design-web-request-flow.png) + +* The request typically begins with a user interaction on the UI (a *use case*) that causes an HTTP request to the server. +* An MVC Controller or a Razor Page Handler in the Presentation Layer (or in the Distributed Services Layer) handles the request and can perform some cross cutting concerns in this stage ([Authorization](Authorization.md), [Validation](Validation.md), [Exception Handling](Exception-Handling.md), etc.). A Controller/Page injects the related Application Service interface and calls its method(s) by sending and receiving DTOs. +* The Application Service uses the Domain Objects (Entities, Repository interfaces, Domain Services, etc.) to implement the *use case*. Application Layer implements some cross cutting concerns (Authorization, Validation, etc.). An Application Service method should be a [Unit Of Work](Unit-Of-Work.md). That means it should be atomic. + +Most of the cross cutting concerns are **automatically and conventionally implemented by the ABP Framework** and you typically don't need to write code for them. + +### Common Principles + +Before going into details, let's see some overall DDD principles; + +#### Database Provider / ORM Independence + +The domain and the application layers should be ORM / Database Provider agnostic. They should only depend on the Repository interfaces and the Repository interfaces don't use any ORM specific objects. + +Here, the main reasons of this principle; + +1. To make your domain/application **infrastructure independent** since the infrastructure may change in the future or you may need to support a second database type later. +2. To make your domain/application **focus on the business code** by hiding the infrastructure details behind the repositories. +3. To make your **automated tests** easier since you can mock the repositories in this case. + +> As a respect to this principle, none of the projects in the solution has reference to the `EntityFrameworkCore` project, except the startup application. + +##### Discussion About the Database Independence Principle + +Especially, the **reason 1** deeply effects your domain **object design** (especially, the entity relations) and **application code**. Assume that you are using [Entity Framework Core](Entity-Framework-Core.md) with a relational database. If you are willing to make your application switchable to [MongoDB](MongoDB.md) later, you can't use some very **useful EF Core features**. Examples; + +* You can't assume [Change Tracking](https://docs.microsoft.com/en-us/ef/core/querying/tracking) since MongoDB provider can't do it. So, you always need to explicitly update the changed entities. +* You can't use [Navigation Properties](https://docs.microsoft.com/en-us/ef/core/modeling/relationships) (or Collections) to other Aggregates in your entities since this is not possible for a Document Database. See the "Rule: Reference Other Aggregates Only By Id" section for more info. + +If you think such features are **important** for you and you **will never stray** from the EF Core, we believe that it is worth **stretching this principle**. We still suggest to use the repository pattern to hide the infrastructure details. But you can assume that you are using EF Core while designing your entity relations and writing your application code. You can even reference to the EF Core NuGet Package from your application layer to be able to directly use the asynchronous LINQ extension methods, like `ToListAsync()` (see the *IQueryable & Async Operations* section in the [Repositories](Repositories.md) document for more info). + +#### Presentation Technology Agnostic + +The presentation technology (UI Framework) is one of the most changed parts of a real world application. It is very important to design the **Domain and Application Layers** to be completely **unaware** of the presentation technology/framework. This principle is relatively easy to implement and ABP's startup template makes it even easier. + +In some cases, you may need to have **duplicate logic** in the application and presentation layers. For example, you may need to duplicate the **validation** and **authorization** checks in both layers. The checks in the UI layer is mostly for **user experience** while checks in the application and domain layers are for **security and data integrity**. That's perfectly normal and necessary. + +#### Focus on the State Changes, Not Reporting + +DDD focuses on how the domain objects **changes and interactions**; How to create an entity and change its properties by preserving the data **integrity/validity** and implementing the **business rules**. + +DDD **ignores reporting** and mass querying. That doesn't mean they are not important. If your application doesn't have fancy dashboards and reports, who would use it? However, reporting is another topic. You typically want to use the full power of the SQL Server or even use a separate data source (like ElasticSearch) for reporting purpose. You will write optimized queries, create indexes and even stored procedures(!). You are free to do all these things as long as you don't infect them into your business logic. + +## Implementation: The Building Blocks + +This is the essential part of this guide. We will introduce and explain some **explicit rules** with examples. You can follow these rules and apply in your solution while implementing the Domain Driven Design. + +### The Example Domain + +The examples will use some concepts those are used by GitHub, like `Issue`, `Repository`, `Label` and `User`, you are already familiar with. The figure below shows some of the aggregates, aggregate roots, entities, value object and the relations between them: + +![domain driven design example schema](images/domain-driven-design-example-domain-schema.png) + +**Issue Aggregate** consists of an `Issue` Aggregate Root that contains `Comment` and `IssueLabel` collections. Other aggregates are shown as simple since we will focus on the Issue Aggregate: + +![domain-driven-design-issue-aggregate-diagram](images/domain-driven-design-issue-aggregate-diagram.png) + +### Aggregates + +As said before, an [Aggregate](Entities.md) is a cluster of objects (entities and value objects) bound together by an Aggregate Root object. This section will introduce the principles and rules related to the Aggregates. + +> We refer the term *Entity* both for *Aggregate Root* and *sub-collection entities* unless we explicitly write *Aggregate Root* or *sub-collection entity*. + +#### Aggregate / Aggregate Root Principles + +##### Business Rules + +Entities are responsible to implement the business rules related to the properties of their own. The *Aggregate Root Entities* are also responsible for their sub-collection entities. + +An aggregate should maintain its self **integrity** and **validity** by implementing domain rules and constraints. That means, unlike the DTOs, Entities have **methods to implement some business logic**. Actually, we should try to implement business rules in the entities wherever possible. + +##### Single Unit + +An aggregate is **retrieved and saved as a single unit**, with all the sub-collections and properties. For example, if you want to add a `Comment` to an `Issue`, you need to; + +* Get the `Issue` from database with including all the sub-collections (`Comment`s and `IssueLabel`s). +* Use methods on the `Issue` class to add a new comment, like `Issue.AddComment(...);`. +* Save the `Issue` (with all sub-collections) to the database as a single database operation (update). + +That may seem strange to the developers used to work with **EF Core & Relational Databases** before. Getting the `Issue` with all details seems **unnecessary and inefficient**. Why don't we just execute an SQL `Insert` command to database without querying any data? + +The answer is that we should **implement the business** rules and preserve the data **consistency** and **integrity** in the **code**. If we have a business rule like "*Users can not comment on the locked issues*", how can we check the `Issue`'s lock state without retrieving it from the database? So, we can execute the business rules only if the related objects available in the application code. + +On the other hand, **MongoDB** developers will find this rule very natural. In MongoDB, an aggregate object (with sub-collections) is saved in a **single collection** in the database (while it is distributed into several tables in a relational database). So, when you get an aggregate, all the sub-collections are already retrieved as a part of the query, without any additional configuration. + +ABP Framework helps to implement this principle in your applications. + +**Example: Add a comment to an issue** + +````csharp +public class IssueAppService : ApplicationService, IIssueAppService +{ + private readonly IRepository _issueRepository; + + public IssueAppService(IRepository issueRepository) + { + _issueRepository = issueRepository; + } + + [Authorize] + public async Task CreateCommentAsync(CreateCommentDto input) + { + var issue = await _issueRepository.GetAsync(input.IssueId); + issue.AddComment(CurrentUser.GetId(), input.Text); + await _issueRepository.UpdateAsync(issue); + } +} +```` + +`_issueRepository.GetAsync` method retrieves the `Issue` with all details (sub-collections) as a single unit by default. While this works out of the box for MongoDB, you need to configure your aggregate details for the EF Core. But, once you configure, repositories automatically handle it. `_issueRepository.GetAsync` method gets an optional parameter, `includeDetails`, that you can pass `false` to disable this behavior when you need it. + +> See the *Loading Related Entities* section of the [EF Core document](Entity-Framework-Core.md) for the configuration and alternative scenarios. + +`Issue.AddComment` gets a `userId` and comment `text`, implements the necessary business rules and adds the comment to the Comments collection of the `Issue`. + +Finally, we use `_issueRepository.UpdateAsync` to save changes to the database. + +> EF Core has a **change tracking** feature. So, you actually don't need to call `_issueRepository.UpdateAsync`. It will be automatically saved thanks to ABP's Unit Of Work system that automatically calls `DbContext.SaveChanges()` at the end of the method. However, for MongoDB, you need to explicitly update the changed entity. +> +> So, if you want to write your code Database Provider independent, you should always call the `UpdateAsync` method for the changed entities. + +##### Transaction Boundary + +An aggregate is generally considered as a transaction boundary. If a use case works with a single aggregate, reads and saves it as a single unit, all the changes made to the aggregate objects are saved together as an atomic operation and you don't need to an explicit database transaction. + +However, in real life, you may need to change **more than one aggregate instances** in a single use case and you need to use database transactions to ensure **atomic update** and **data consistency**. Because of that, ABP Framework uses an explicit database transaction for a use case (an application service method boundary). See the [Unit Of Work](Unit-Of-Work.md) documentation for more info. + +##### Serializability + +An aggregate (with the root entity and sub-collections) should be serializable and transferrable on the wire as a single unit. For example, MongoDB serializes the aggregate to JSON document while saving to the database and deserializes from JSON while reading from the database. + +> This requirement is not necessary when you use relational databases and ORMs. However, it is an important practice of Domain Driven Design. + +The following rules will already bring the serializability. + +#### Aggregate / Aggregate Root Rules & Best Practices + +The following rules ensures implementing the principles introduced above. + +##### Reference Other Aggregates Only By Id + +The first rule says an Aggregate should reference to other aggregates only by their Id. That means you can not add navigation properties to other aggregates. + +* This rule makes it possible to implement the serializability principle. +* It also prevents different aggregates manipulate each other and leaking business logic of an aggregate to one another. + +You see two aggregate roots, `GitRepository` and `Issue` in the example below; + +![domain-driven-design-reference-by-id-sample](images/domain-driven-design-reference-by-id-sample.png) + +* `GitRepository` should not have a collection of the `Issue`s since they are different aggregates. +* `Issue` should not have a navigation property for the related `GitRepository` since it is a different aggregate. +* `Issue` can have `RepositoryId` (as a `Guid`). + +So, when you have an `Issue` and need to have `GitRepository` related to this issue, you need to explicitly query it from database by the `RepositoryId`. + +###### For EF Core & Relational Databases + +In MongoDB, it is naturally not suitable to have such navigation properties/collections. If you do that, you find a copy of the destination aggregate object in the database collection of the source aggregate since it is being serialized to JSON on save. + +However, EF Core & relational database developers may find this restrictive rule unnecessary since EF Core can handle it on database read and write. We see this an important rule that helps to **reduce the complexity** of the domain prevents potential problems and we strongly suggest to implement this rule. However, if you think it is practical to ignore this rule, see the *Discussion About the Database Independence Principle* section above. + +##### Keep Aggregates Small + +One good practice is to keep an aggregate simple and small. This is because an aggregate will be loaded and saved as a single unit and reading/writing a big object has performance problems. See the example below: + +![domain-driven-design-aggregate-keep-small](images/domain-driven-design-aggregate-keep-small.png) + +Role aggregate has a collection of `UserRole` value objects to track the users assigned for this role. Notice that `UserRole` is not another aggregate and it is not a problem for the rule *Reference Other Aggregates Only By Id*. However, it is a problem in practical. A role may be assigned to thousands (even millions) of users in a real life scenario and it is a significant performance problem to load thousands of items whenever you query a `Role` from database (remember: Aggregates are loaded by their sub-collections as a single unit). + +On the other hand, `User` may have such a `Roles` collection since a user doesn't have much roles in practical and it can be useful to have a list of roles while you are working with a User Aggregate. + +If you think carefully, there is one more problem when Role and User both have the list of relation when use a **non-relational database, like MongoDB**. In this case, the same information is duplicated in different collections and it will be hard to maintain data consistency (whenever you add an item to `User.Roles`, you need to add it to `Role.Users` too). + +So, determine your aggregate boundaries and size based on the following considerations; + +* Objects used together. +* Query (load/save) performance and memory consumption. +* Data integrity, validity and consistency. + +In practical; + +* Most of the aggregate roots will **not have sub-collections**. +* A sub-collection should not have more than **100-150 items** inside it at the most case. If you think a collection potentially can have more items, don't define the collection as a part of the aggregate and consider to extract another aggregate root for the entity inside the collection. + +##### Primary Keys of the Aggregate Roots / Entities + +* An aggregate root typically has a single `Id` property for its identifier (Primark Key: PK). We prefer `Guid` as the PK of an aggregate root entity (see the [Guid Genertation document](Guid-Generation.md) to learn why). +* An entity (that's not the aggregate root) in an aggregate can use a composite primary key. + +For example, see the Aggregate root and the Entity below: + +![domain-driven-design-entity-primary-keys](images/domain-driven-design-entity-primary-keys.png) + +* `Organization` has a `Guid` identifier (`Id`). +* `OrganizationUser` is a sub-collection of an `Organization` and has a composite primary key consists of the `OrganizationId` and `UserId`. + +That doesn't mean sub-collection entities should always have composite PKs. They may have single `Id` properties when it's needed. + +> Composite PKs are actually a concept of relational databases since the sub-collection entities have their own tables and needs to a PK. On the other hand, for example, in MongoDB you don't need to define PK for the sub-collection entities at all since they are stored as a part of the aggregate root. + +##### Constructors of the Aggregate Roots / Entities + +The constructor is located where the lifecycle of an entity begins. There are a some responsibilities of a well designed constructor: + +* Gets the **required entity properties** as parameters to **create a valid entity**. Should force to pass only for the required parameters and may get non-required properties as optional parameters. +* **Checks validity** of the parameters. +* Initializes **sub-collections**. + +**Example: `Issue` (Aggregate Root) constructor** + +````csharp +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Volo.Abp; +using Volo.Abp.Domain.Entities; + +namespace IssueTracking.Issues +{ + public class Issue : AggregateRoot + { + public Guid RepositoryId { get; set; } + public string Title { get; set; } + public string Text { get; set; } + public Guid? AssignedUserId { get; set; } + public bool IsClosed { get; set; } + public IssueCloseReason? CloseReason { get; set; } //enum + + public ICollection Labels { get; set; } + + public Issue( + Guid id, + Guid repositoryId, + string title, + string text = null, + Guid? assignedUserId = null + ) : base(id) + { + RepositoryId = repositoryId; + Title = Check.NotNullOrWhiteSpace(title, nameof(title)); + + Text = text; + AssignedUserId = assignedUserId; + + Labels = new Collection(); + } + + private Issue() { /* for deserialization & ORMs */ } + } +} +```` + +* `Issue` class properly **forces to create a valid entity** by getting minimum required properties in its constructor as parameters. +* The constructor **validates** the inputs (`Check.NotNullOrWhiteSpace(...)` throws `ArgumentException` if the given value is empty). +* It **initializes the sub-collections**, so you don't get a null reference exception when you try to use the `Labels` collection after creating the `Issue`. +* The constructor also **takes the `id`** and passes to the `base` class. We don't generate `Guid`s inside the constructor to be able to delegate this responsibility to another service (see [Guid Generation](Guid-Generation.md)). +* Private **empty constructor** is necessary for ORMs. We made it `private` to prevent accidently using it in our own code. + +> See the [Entities](Entities.md) document to learn more about creating entities with the ABP Framework. + +##### Entity Property Accessors & Methods + +The example above may seem strange to you! For example, we force to pass a non-null `Title` in the constructor. However, the developer may then set the `Title` property to `null` without any control. This is because the example code above just focuses on the constructor. + +If we declare all the properties with **public setters** (like the example `Issue` class above), we can't force **validity** and **integrity** of the entity in its lifecycle. So; + +* Use **private setter** for a property when you need to perform any **logic** while setting that property. +* Define public methods to manipulate such properties. + +**Example: Methods to change the properties in a controlled way** + +````csharp +using System; +using Volo.Abp; +using Volo.Abp.Domain.Entities; + +namespace IssueTracking.Issues +{ + public class Issue : AggregateRoot + { + public Guid RepositoryId { get; private set; } //Never changes + public string Title { get; private set; } //Needs validation + public string Text { get; set; } //No validation + public Guid? AssignedUserId { get; set; } //No validation + public bool IsClosed { get; private set; } //Should be changed with CloseReason + public IssueCloseReason? CloseReason { get; private set; } //Should be changed with IsClosed + + //... + + public void SetTitle(string title) + { + Title = Check.NotNullOrWhiteSpace(title, nameof(title)); + } + + public void Close(IssueCloseReason reason) + { + IsClosed = true; + CloseReason = reason; + } + + public void ReOpen() + { + IsClosed = false; + CloseReason = null; + } + } +} +```` + +* `RepositoryId` setter made private and there is no way to change it after creating an `Issue` because this is what we want in this domain: An issue can't be moved to another repository. +* `Title` setter made private and `SetTitle` method has been created if you want to change it later in a controlled way. +* `Text` and `AssignedUserId` has public setters since there is no restriction on them. They can be null or any other value. We think it is unnecessary to define separate methods to set them. If we need later, we can add methods and make the setters private. Breaking changes are not problem in the domain layer since the domain layer is an internal project, it is not exposed to clients. +* `IsClosed` and `IssueCloseReason` are pair properties. Defined `Close` and `ReOpen` methods to change them together. In this way, we prevent to close an issue without any reason. + +##### Business Logic & Exceptions in the Entities + +When you implement validation and business logic in the entities, you frequently need to manage the exceptional cases. In these cases; + +* Create **domain specific exceptions**. +* **Throw these exceptions** in the entity methods when necessary. + +**Example** + +````csharp +public class Issue : AggregateRoot +{ + //... + + public bool IsLocked { get; private set; } + public bool IsClosed { get; private set; } + public IssueCloseReason? CloseReason { get; private set; } + + public void Close(IssueCloseReason reason) + { + IsClosed = true; + CloseReason = reason; + } + + public void ReOpen() + { + if (IsLocked) + { + throw new IssueStateException( + "Can not open a locked issue! Unlock it first." + ); + } + + IsClosed = false; + CloseReason = null; + } + + public void Lock() + { + if (!IsClosed) + { + throw new IssueStateException( + "Can not open a locked issue! Unlock it first." + ); + } + + IsLocked = true; + } + + public void Unlock() + { + IsLocked = false; + } +} +```` + +There are two business rules here; + +* A locked issue can not be re-opened. +* You can not lock an open issue. + +`Issue` class throws an `IssueStateException` in these cases to force the business rules: + +````csharp +using System; + +namespace IssueTracking.Issues +{ + public class IssueStateException : Exception + { + public IssueStateException(string message) + : base(message) + { + + } + } +} +```` + +There are two potential problems of throwing such exceptions; + +1. In case of such an exception, should the **end user** see the exception (error) message? If so, how do you **localize** the exception message? You can not use the [localization](Localization.md) system, because you can't inject and use `IStringLocalizer` in the entities. +2. For a web application or HTTP API, what **HTTP Status Code** should return to the client? + +ABP's [Exception Handling](Exception-Handling.md) system solves these and similar problems. + +**Example: Throwing a business exception with code** + +````csharp +using Volo.Abp; + +namespace IssueTracking.Issues +{ + public class IssueStateException : BusinessException + { + public IssueStateException(string code) + : base(code) + { + + } + } +} +```` + +* `IssueStateException` class inherits the `BusinessException` class. ABP returns 403 (forbidden) HTTP Status code by default (instead of 500 - Internal Server Error) for the exceptions derived from the `BusinessException`. +* The `code` is used as a key in the localization resource file to find the localized message. + +Now, we can change the `ReOpen` method as shown below: + +````csharp +public void ReOpen() +{ + if (IsLocked) + { + throw new IssueStateException("IssueTracking:CanNotOpenLockedIssue"); + } + + IsClosed = false; + CloseReason = null; +} +```` + +> Use constants instead of magic strings. + +And add an entry to the localization resource like below: + +````json +"IssueTracking:CanNotOpenLockedIssue": "Can not open a locked issue! Unlock it first." +```` + +* When you throw the exception, ABP automatically uses this localized message (based on the current language) to show to the end user. +* The exception code (`IssueTracking:CanNotOpenLockedIssue` here) is also sent to the client, so it may handle the error case programmatically. + +> For this example, you could directly throw `BusinessException` instead of defining a specialized `IssueStateException`. The result will be same. See the [exception handling document](Exception-Handling.md) for all the details. + +##### Business Logic in Entities Requiring External Services + +It is simple to implement a business rule in an entity method when the business logic only uses the properties of that entity. What if the business logic requires to **query database** or **use any external services** that should be resolved from the [dependency injection](Dependency-Injection.md) system. Remember; **Entities can not inject services!** + +There are two common ways of implementing such a business logic: + +* Implement the business logic on an entity method and **get external dependencies as parameters** of the method. +* Create a **Domain Service**. + +Domain Services will be explained later. But, now let's see how it can be implemented in the entity class. + +**Example: Business Rule: Can not assign more than 3 open issues to a user concurrently** + +````csharp +public class Issue : AggregateRoot +{ + //... + public Guid? AssignedUserId { get; private set; } + + public async Task AssignToAsync(AppUser user, IUserIssueService userIssueService) + { + var openIssueCount = await userIssueService.GetOpenIssueCountAsync(user.Id); + + if (openIssueCount >= 3) + { + throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit"); + } + + AssignedUserId = user.Id; + } + + public void CleanAssignment() + { + AssignedUserId = null; + } +} +```` + +* `AssignedUserId` property setter made private. So, the only way to change it to use the `AssignToAsync` and `CleanAssignment` methods. +* `AssignToAsync` gets an `AppUser` entity. Actually, it only uses the `user.Id`, so you could get a `Guid` value, like `userId`. However, this way ensures that the `Guid` value is `Id` of an existing user and not a random `Guid` value. +* `IUserIssueService` is an arbitrary service that is used to get open issue count for a user. It's the responsibility of the code part (that calls the `AssignToAsync`) to resolve the `IUserIssueService` and pass here. +* `AssignToAsync` throws exception if the business rule doesn't meet. +* Finally, if everything is correct, `AssignedUserId` property is set. + +This method perfectly guarantees to apply the business logic when you want to assign an issue to a user. However, it has some problems; + +* It makes the entity class **depending on an external service** which makes the entity **complicated**. +* It makes **hard to use** the entity. The code that uses the entity now needs to inject `IUserIssueService` and pass to the `AssignToAsync` method. + +An alternative way of implementing this business logic is to introduce a **Domain Service**, which will be explained later. + +### Repositories + +A [Repository](Repositories.md) is a collection-like interface that is used by the Domain and Application Layers to access to the data persistence system (the database) to read and write the Business Objects, generally the Aggregates. + +Common Repository principles are; + +* Define a repository **interface in the Domain Layer** (because it is used in the Domain and Application Layers), **implement in the Infrastructure Layer** (*EntityFrameworkCore* project in the startup template). +* **Do not include business logic** inside the repositories. +* Repository interface should be **database provider / ORM independent**. For example, do not return a `DbSet` from a repository method. `DbSet` is an object provided by the EF Core. +* **Create repositories for aggregate roots**, not for all entities. Because, sub-collection entities (of an aggregate) should be accessed over the aggregate root. + +#### Do Not Include Domain Logic in Repositories + +While this rule seems obvious at the beginning, it is easy to leak business logic into repositories. + +**Example: Get inactive issues from a repository** + +````csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace IssueTracking.Issues +{ + public interface IIssueRepository : IRepository + { + Task> GetInActiveIssuesAsync(); + } +} +```` + +`IIssueRepository` extends the standard `IRepository<...>` interface by adding a `GetInActiveIssuesAsync` method. This repository works with such an `Issue` class: + +````csharp +public class Issue : AggregateRoot, IHasCreationTime +{ + public bool IsClosed { get; private set; } + public Guid? AssignedUserId { get; private set; } + public DateTime CreationTime { get; private set; } + public DateTime? LastCommentTime { get; private set; } + //... +} +```` + +(the code shows only the properties we need for this example) + +The rule says the repository shouldn't know the business rules. The question here is "**What is an inactive issue**? Is it a business rule definition?" + +Let's see the implementation to understand it: + +````csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using IssueTracking.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace IssueTracking.Issues +{ + public class EfCoreIssueRepository : + EfCoreRepository, + IIssueRepository + { + public EfCoreIssueRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task> GetInActiveIssuesAsync() + { + var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30)); + + return await DbSet.Where(i => + + //Open + !i.IsClosed && + + //Assigned to nobody + i.AssignedUserId == null && + + //Created 30+ days ago + i.CreationTime < daysAgo30 && + + //No comment or the last comment was 30+ days ago + (i.LastCommentTime == null || i.LastCommentTime < daysAgo30) + + ).ToListAsync(); + } + } +} +```` + +(Used EF Core for the implementation. See the [EF Core integration document](Entity-Framework-Core.md) to learn how to create custom repositories with the EF Core.) + +When we check the `GetInActiveIssuesAsync` implementation, we see a **business rule that defines an in-active issue**: The issue should be **open**, **assigned to nobody**, **created 30+ days ago** and has **no comment in the last 30 days**. + +This is an implicit definition of a business rule that is hidden inside a repository method. The problem occurs when we need to reuse this business logic. + +For example, let's say that we want to add an `bool IsInActive()` method on the `Issue` entity. In this way, we can check activeness when we have an issue entity. + +Let's see the implementation: + +````csharp +public class Issue : AggregateRoot, IHasCreationTime +{ + public bool IsClosed { get; private set; } + public Guid? AssignedUserId { get; private set; } + public DateTime CreationTime { get; private set; } + public DateTime? LastCommentTime { get; private set; } + //... + + public bool IsInActive() + { + var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30)); + return + //Open + !IsClosed && + + //Assigned to nobody + AssignedUserId == null && + + //Created 30+ days ago + CreationTime < daysAgo30 && + + //No comment or the last comment was 30+ days ago + (LastCommentTime == null || LastCommentTime < daysAgo30); + } +} +```` + +We had to copy/paste/modify the code. What if the definition of the activeness changes? We should not forget to update both places. This is a duplication of a business logic, which is pretty dangerous. + +A good solution to this problem is the *Specification Pattern*! + +### Specifications + +A [specification](Specifications.md) is a **named**, **reusable**, **combinable** and **testable** class to filter the Domain Objects based on the business rules. + +ABP Framework provides necessary infrastructure to easily create specification classes and use them inside your application code. Let's implement the in-active issue filter as a specification class: + +````csharp +using System; +using System.Linq.Expressions; +using Volo.Abp.Specifications; + +namespace IssueTracking.Issues +{ + public class InActiveIssueSpecification : Specification + { + public override Expression> ToExpression() + { + var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30)); + return i => + + //Open + !i.IsClosed && + + //Assigned to nobody + i.AssignedUserId == null && + + //Created 30+ days ago + i.CreationTime < daysAgo30 && + + //No comment or the last comment was 30+ days ago + (i.LastCommentTime == null || i.LastCommentTime < daysAgo30); + } + } +} +```` + +`Specification` base class simplifies to create a specification class by defining an expression. Just moved the expression here, from the repository. + +Now, we can re-use the `InActiveIssueSpecification` in the `Issue` entity and `EfCoreIssueRepository` classes. + +#### Using within the Entity + +`Specification` class provides an `IsSatisfiedBy` method that returns `true` if the given object (entity) satisfies the specification. We can re-write the `Issue.IsInActive` method as shown below: + +````csharp +public class Issue : AggregateRoot, IHasCreationTime +{ + public bool IsClosed { get; private set; } + public Guid? AssignedUserId { get; private set; } + public DateTime CreationTime { get; private set; } + public DateTime? LastCommentTime { get; private set; } + //... + + public bool IsInActive() + { + return new InActiveIssueSpecification().IsSatisfiedBy(this); + } +} +```` + +Just created a new instance of the `InActiveIssueSpecification` and used its `IsSatisfiedBy` method to re-use the expression defined by the specification. + +#### Using with the Repositories + +First, starting from the repository interface: + +````csharp +public interface IIssueRepository : IRepository +{ + Task> GetIssuesAsync(ISpecification spec); +} +```` + +Renamed `GetInActiveIssuesAsync` to simple `GetIssuesAsync` by taking a specification object. Since the **specification (the filter) has been moved out of the repository**, we no longer need to create different methods to get issues with different conditions (like `GetAssignedIssues(...)`, `GetLockedIssues(...)`, etc.) + +Updated implementation of the repository can be like that: + +````csharp +public class EfCoreIssueRepository : + EfCoreRepository, + IIssueRepository +{ + public EfCoreIssueRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task> GetIssuesAsync(ISpecification spec) + { + return await DbSet + .Where(spec.ToExpression()) + .ToListAsync(); + } +} +```` + +Since `ToExpression()` method returns an expression, it can be directly passed to the `Where` method to filter the entities. + +Finally, we can pass any Specification instance to the `GetIssuesAsync` method: + +````csharp +public class IssueAppService : ApplicationService, IIssueAppService +{ + private readonly IIssueRepository _issueRepository; + + public IssueAppService(IIssueRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task DoItAsync() + { + var issues = await _issueRepository.GetIssuesAsync( + new InActiveIssueSpecification() + ); + } +} +```` + +##### With Default Repositories + +Actually, you don't have to create custom repositories to be able to use specifications. The standard `IRepository` already extends the `IQueryable`, so you can use the standard LINQ extension methods over it: + +````csharp +public class IssueAppService : ApplicationService, IIssueAppService +{ + private readonly IRepository _issueRepository; + + public IssueAppService(IRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task DoItAsync() + { + var issues = AsyncExecuter.ToListAsync( + _issueRepository.Where(new InActiveIssueSpecification()) + ); + } +} +```` + +`AsyncExecuter` is a utility provided by the ABP Framework to use asynchronous LINQ extension methods (like `ToListAsync` here) without depending on the EF Core NuGet package. See the [Repositories document](Repositories.md) for more information. + +#### Combining the Specifications + +One powerful side of the Specifications is they are combinable. Assume that we have another specification that returns `true` only if the `Issue` is in a Milestone: + +````csharp +public class MilestoneSpecification : Specification +{ + public Guid MilestoneId { get; } + + public MilestoneSpecification(Guid milestoneId) + { + MilestoneId = milestoneId; + } + + public override Expression> ToExpression() + { + return i => i.MilestoneId == MilestoneId; + } +} +```` + +This Specification is *parametric* as a difference from the `InActiveIssueSpecification`. We can combine both specifications to get a list of inactive issues in a specific milestone: + +````csharp +public class IssueAppService : ApplicationService, IIssueAppService +{ + private readonly IRepository _issueRepository; + + public IssueAppService(IRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task DoItAsync(Guid milestoneId) + { + var issues = AsyncExecuter.ToListAsync( + _issueRepository + .Where( + new InActiveIssueSpecification() + .And(new MilestoneSpecification(milestoneId)) + .ToExpression() + ) + ); + } +} +```` + +The example above uses the `And` extension method to combine the specifications. There are more combining methods are available, like `Or(...)` and `AndNot(...)`. + +> See the [Specifications document](Specifications.md) for more details about the specification infrastructure provided by the ABP Framework. + +### Domain Services + +Domain Services implement domain logic which; + +* Depends on **services and repositories**. +* Needs to work with **multiple aggregates**, so the logic doesn't properly fit in any of the aggregates. + +Domain Services work with Domain Objects. Their methods can **get and return entities, value objects, primitive types**... etc. However, **they don't get/return DTOs**. DTOs is a part of the Application Layer. + +**Example: Assigning an issue to a user** + +Remember how an issue assignment has been implemented in the `Issue` entity: + +````csharp +public class Issue : AggregateRoot +{ + //... + public Guid? AssignedUserId { get; private set; } + + public async Task AssignToAsync(AppUser user, IUserIssueService userIssueService) + { + var openIssueCount = await userIssueService.GetOpenIssueCountAsync(user.Id); + + if (openIssueCount >= 3) + { + throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit"); + } + + AssignedUserId = user.Id; + } + + public void CleanAssignment() + { + AssignedUserId = null; + } +} +```` + +Here, we will move this logic into a Domain Service. + +First, changing the `Issue` class: + +````csharp +public class Issue : AggregateRoot +{ + //... + public Guid? AssignedUserId { get; internal set; } +} +```` + +* Removed the assign-related methods. +* Changed `AssignedUserId` property's setter from `private` to `internal`, to allow to set it from the Domain Service. + +The next step is to create a domain service, named `IssueManager`, that has `AssignToAsync` to assign the given issue to the given user. + +````csharp +public class IssueManager : DomainService +{ + private readonly IRepository _issueRepository; + + public IssueManager(IRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task AssignToAsync(Issue issue, AppUser user) + { + var openIssueCount = await _issueRepository.CountAsync( + i => i.AssignedUserId == user.Id && !i.IsClosed + ); + + if (openIssueCount >= 3) + { + throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit"); + } + + issue.AssignedUserId = user.Id; + } +} +```` + +`IssueManager` can inject any service dependency and use to query open issue count on the user. + +> We prefer and suggest to use the `Manager` suffix for the Domain Services. + +The only problem of this design is that `Issue.AssignedUserId` is now open to set out of the class. However, it is not `public`. It is `internal` and changing it is possible only inside the same Assembly, the `IssueTracking.Domain` project for this example solution. We think this is reasonable; + +* Domain Layer developers are already aware of domain rules and they use the `IssueManager`. +* Application Layer developers are already forces to use the `IssueManager` since they don't directly set it. + +While there is a tradeoff between two approaches, we prefer to create Domain Services when the business logic requires to work with external services. + +> If you don't have a good reason, we think **there is no need to create interfaces** (like `IIssueManager` for the `IssueManager`) for Domain Services. + +### Application Services + +An [Application Service](Application-Services.md) is a stateless service that implements **use cases** of the application. An application service typically **gets and returns DTOs**. It is used by the Presentation Layer. It **uses and coordinates the domain objects** (entities, repositories, etc.) to implement the use cases. + +Common principles of an application service are; + +* Implement the **application logic** that is specific to the current use-case. Do not implement the core domain logic inside the application services. We will come back to differences between Application Domain logics. +* **Never get or return entities** for an application service method. This breaks the encapsulation of the Domain Layer. Always get and return DTOs. + +**Example: Assign an Issue to a User** + +````csharp +using System; +using System.Threading.Tasks; +using IssueTracking.Users; +using Microsoft.AspNetCore.Authorization; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace IssueTracking.Issues +{ + public class IssueAppService : ApplicationService, IIssueAppService + { + private readonly IssueManager _issueManager; + private readonly IRepository _issueRepository; + private readonly IRepository _userRepository; + + public IssueAppService( + IssueManager issueManager, + IRepository issueRepository, + IRepository userRepository) + { + _issueManager = issueManager; + _issueRepository = issueRepository; + _userRepository = userRepository; + } + + [Authorize] + public async Task AssignAsync(IssueAssignDto input) + { + var issue = await _issueRepository.GetAsync(input.IssueId); + var user = await _userRepository.GetAsync(input.UserId); + + await _issueManager.AssignToAsync(issue, user); + + await _issueRepository.UpdateAsync(issue); + } + } +} +```` + +An application service method typically has three steps those are implemented here; + +1. Get the related domain objects from database to implement the use case. +2. Use domain objects (domain services, entities, etc.) to perform the actual operation. +3. Update the changed entities in the database. + +> The last *Update* is not necessary if your are using EF Core since it has a Change Tracking system. If you want to take advantage of this EF Core feature, please see the *Discussion About the Database Independence Principle* section above. + +`IssueAssignDto` in this example is a simple DTO class: + +````csharp +using System; + +namespace IssueTracking.Issues +{ + public class IssueAssignDto + { + public Guid IssueId { get; set; } + public Guid UserId { get; set; } + } +} +```` + +### Data Transfer Objects + +A [DTO](Data-Transfer-Objects.md) is a simple object that is used to transfer state (data) between the Application and Presentation Layers. So, Application Service methods gets and returns DTOs. + +#### Common DTO Principles & Best Practices + +* A DTO **should be serializable**, by its nature. Because, most of the time it is transferred over network. So, it should have a **parameterless (empty) constructor**. +* Should not contain any **business logic**. +* **Never** inherit from or reference to **entities**. + +**Input DTOs** (those are passed to the Application Service methods) have different natures than **Output DTOs** (those are returned from the Application Service methods). So, they will be treated differently. + +#### Input DTO Best Practices + +##### Do not Define Unused Properties for Input DTOs + +Define **only the properties needed** for the use case! Otherwise, it will be **confusing for the clients** to use the Application Service method. You can surely define **optional properties**, but they should effect how the use case is working, when the client provides them. + +This rule seems unnecessary first. Who would define unused parameters (input DTO properties) for a method? But it happens, especially when you try to reuse input DTOs. + +##### Do not Re-Use Input DTOs + +Define a **specialized input DTO for each use case** (Application Service method). Otherwise, some properties are not used in some cases and this violates the rule defined above: *Do not Define Unused Properties for Input DTOs*. + +Sometimes, it seems appealing to reuse the same DTO class for two use cases, because they are almost same. Even if they are same now, they will probably become different by the time and you will come to the same problem. **Code duplication is a better practice than coupling use cases**. + +Another way of reusing input DTOs is **inheriting** DTOs from each other. While this can be useful in some rare cases, most of the time it brings you to the same point. + +**Example: User Application Service** + +````csharp +public interface IUserAppService : IApplicationService +{ + Task CreateAsync(UserDto input); + Task UpdateAsync(UserDto input); + Task ChangePasswordAsync(UserDto input); +} +```` + +`IUserAppService` uses `UserDto` as the input DTO in all methods (use cases). `UserDto` is defined below: + +````csharp +public class UserDto +{ + public Guid Id { get; set; } + public string UserName { get; set; } + public string Email { get; set; } + public string Password { get; set; } + public DateTime CreationTime { get; set; } +} +```` + +For this example; + +* `Id` is not used in *Create* since the server determines it. +* `Password` is not used in *Update* since we have another method for it. +* `CreationTime` is never used since we can't allow client to send the Creation Time. It should be set in the server. + +A true implementation can be like that: + +````csharp +public interface IUserAppService : IApplicationService +{ + Task CreateAsync(UserCreationDto input); + Task UpdateAsync(UserUpdateDto input); + Task ChangePasswordAsync(UserChangePasswordDto input); +} +```` + +With the given input DTO classes: + +````csharp +public class UserCreationDto +{ + public string UserName { get; set; } + public string Email { get; set; } + public string Password { get; set; } +} + +public class UserUpdateDto +{ + public Guid Id { get; set; } + public string UserName { get; set; } + public string Email { get; set; } +} + +public class UserChangePasswordDto +{ + public Guid Id { get; set; } + public string Password { get; set; } +} +```` + +This is more maintainable approach although more code is written. + +**Exceptional Case**: There can be some exceptions for this rule: If you always want to develop two methods **in parallel**, they may share the same input DTO (by inheritance or direct reuse). For example, if you have a reporting page that has some filters and you have multiple Application Service methods (like screen report, excel report and csv report methods) use the same filters but returns different results, you may want to reuse the same filter input DTO to **couple these use cases**. Because, in this example, whenever you change a filter, you have to make the necessary changes in all the methods to have a consistent reporting system. + +##### Input DTO Validation Logic + +* Implement only **formal validation** inside the DTO. Use Data Annotation Validation Attributes or implement `IValidatableObject` for formal validation. +* **Do not perform domain validation**. For example, don't try to check unique username constraint in the DTOs. + +**Example: Using Data Annotation Attributes** + +````csharp +using System.ComponentModel.DataAnnotations; + +namespace IssueTracking.Users +{ + public class UserCreationDto + { + [Required] + [StringLength(UserConsts.MaxUserNameLength)] + public string UserName { get; set; } + + [Required] + [EmailAddress] + [StringLength(UserConsts.MaxEmailLength)] + public string Email { get; set; } + + [Required] + [StringLength(UserConsts.MaxEmailLength, + MinimumLength = UserConsts.MinPasswordLength)] + public string Password { get; set; } + } +} +```` + +ABP Framework automatically validates input DTOs, throws `AbpValidationException` and returns HTTP Status `400` to the client in case of an invalid input. + +> Some developers think it is better to separate the validation rules and DTO classes. We think the declarative (Data Annotation) approach is practical and useful and doesn't cause any design problem. However, ABP also supports [FluentValidation integration](FluentValidation.md) if you prefer the other approach. + +> See the [Validation document](Validation.md) for all validation options. + +#### Output DTO Best Practices + +* Keep output **DTO count minimum**. **Reuse** where possible (exception: Do not reuse input DTOs as output DTOs). +* Output DTOs can contain **more properties** than used in the client code. +* Return entity DTO from **Create** and **Update** methods. + +The main goals of these suggestions are; + +* Make client code easy to develop and extend; + * Dealing with **similar, but not same** DTOs are problematic on the client side. + * It is common to **need to other properties** on the UI/client in the future. Returning all properties (by considering security and privileges) of an entity makes client code easy to improve without requiring to touch to the backend code. + * If you are opening your API to **3rd-party clients** that you don't know requirements of each client. +* Make the server side code easy to develop and extend; + * You have less class to **understand and maintain**. + * You can reuse the Entity->DTO **object mapping** code. + * Returning same types from different methods make it easy and clear to create **new methods**. + +**Example: Returning Different DTO types from different methods** + +````csharp +public interface IUserAppService : IApplicationService +{ + UserDto Get(Guid id); + List GetUserNameAndEmail(Guid id); + List GetRoles(Guid id); + List GetList(); + UserCreateResultDto Create(UserCreationDto input); + UserUpdateResultDto Update(UserUpdateDto input); +} +```` + +*(We didn't use async methods to make the example cleaner, but use async in your real world application!)* + +The example code above returns different DTO types for each method. As you can guess, there will be a lot of code duplications for querying data, mapping entities to DTOs. + +The `IUserAppService` service above can be simplified: + +````csharp +public interface IUserAppService : IApplicationService +{ + UserDto Get(Guid id); + List GetList(); + UserDto Create(UserCreationDto input); + UserDto Update(UserUpdateDto input); +} +```` + +With a single output DTO: + +````csharp +public class UserDto +{ + public Guid Id { get; set; } + public string UserName { get; set; } + public string Email { get; set; } + public DateTime CreationTime { get; set; } + public List Roles { get; set; } +} +```` + +* Removed `GetUserNameAndEmail` and `GetRoles` since `Get` method already returns the necessary information. +* `GetList` now returns the same with `Get`. +* `Create` and `Update` also returns the same `UserDto`. + +Using the same DTO has a lot of advantages as explained before. For example, think a scenario where you show a **data grid** of Users on the UI. After updating a user, you can get the return value and **update it on the UI**. So, you don't need to call `GetList` again. This is why we suggest to return the entity DTO (`UserDto` here) as return value from the `Create` and `Update` operations. + +##### Discussion + +Some of the output DTO suggestions may not fit in every scenario. These suggestions can be ignored for **performance** reasons, especially when **large data sets** returned or when you create services for your own UI and you have **too many concurrent requests**. + +In these cases, you may want to create **specialized output DTOs with minimal information**. The suggestions above are especially for applications where **maintaining the codebase** is more important than **negligible performance lost**. + +#### Object to Object Mapping + +Automatic [object to object mapping](Object-To-Object-Mapping.md) is a useful approach to copy values from one object to another when two objects have same or similar properties. + +DTO and Entity classes generally have same/similar properties and you typically need to create DTO objects from Entities. ABP's [object to object mapping system](Object-To-Object-Mapping.md) with [AutoMapper](http://automapper.org/) integration makes these operations much easier comparing to manual mapping. + +* **Use** auto object mapping only for **Entity to output DTO** mappings. +* **Do not use** auto object mapping for **input DTO to Entity** mappings. + +There are some reasons why you **should not use** input DTO to Entity auto mapping; + +1. An Entity class typically has a **constructor** that takes parameters and ensures valid object creation. Auto object mapping operation generally requires an empty constructor. +2. Most of the entity properties will have **private setters** and you should use methods to change these properties in a controlled way. +3. You typically need to **carefully validate and process** the user/client input rather than blindly mapping to the entity properties. + +While some of these problems can be solved through mapping configurations (For example, AutoMapper allows to define custom mapping rules), it makes your business code **implicit/hidden** and **tightly coupled** to the infrastructure. We think the business code should be explicit, clear and easy to understand. + +See the *Entity Creation* section below for an example implementation of the suggestions made in this section. + +## Example Use Cases + +This section will demonstrate some example use cases and discuss alternative scenarios. + +### Entity Creation + +Creating an object from an Entity / Aggregate Root class is the first step of the lifecycle of that entity. The *Aggregate / Aggregate Root Rules & Best Practices* section suggests to **create a primary constructor** for the Entity class that guarantees to **create a valid entity**. So, whenever we need to create an instance of that entity, we should always **use that constructor**. + +See the `Issue` Aggregate Root class below: + +````csharp +public class Issue : AggregateRoot +{ + public Guid RepositoryId { get; private set; } + public string Title { get; private set; } + public string Text { get; set; } + public Guid? AssignedUserId { get; internal set; } + + public Issue( + Guid id, + Guid repositoryId, + string title, + string text = null + ) : base(id) + { + RepositoryId = repositoryId; + Title = Check.NotNullOrWhiteSpace(title, nameof(title)); + Text = text; //Allow empty/null + } + + private Issue() { /* Empty constructor is for ORMs */ } + + public void SetTitle(string title) + { + Title = Check.NotNullOrWhiteSpace(title, nameof(title)); + } + + //... +} +```` + +* This class guarantees to create a valid entity by its constructor. +* If you need to change the `Title` later, you need to use the `SetTitle` method which continues to keep `Title` in a valid state. +* If you want to assign this issue to a user, you need to use `IssueManager` (it implements some business rules before the assignment - see the *Domain Services* section above to remember). +* The `Text` property has a public setter, because it also accepts null values and does not have any validation rules for this example. It is also optional in the constructor. + +Let's see an Application Service method that is used to create an issue: + +````csharp +public class IssueAppService : ApplicationService, IIssueAppService +{ + private readonly IssueManager _issueManager; + private readonly IRepository _issueRepository; + private readonly IRepository _userRepository; + + public IssueAppService( + IssueManager issueManager, + IRepository issueRepository, + IRepository userRepository) + { + _issueManager = issueManager; + _issueRepository = issueRepository; + _userRepository = userRepository; + } + + public async Task CreateAsync(IssueCreationDto input) + { + // Create a valid entity + var issue = new Issue( + GuidGenerator.Create(), + input.RepositoryId, + input.Title, + input.Text + ); + + // Apply additional domain actions + if (input.AssignedUserId.HasValue) + { + var user = await _userRepository.GetAsync(input.AssignedUserId.Value); + await _issueManager.AssignToAsync(issue, user); + } + + // Save + await _issueRepository.InsertAsync(issue); + + // Return a DTO represents the new Issue + return ObjectMapper.Map(issue); + } +} +```` + +`CreateAsync` method; + +* Uses the `Issue` **constructor** to create a valid issue. It passes the `Id` using the [IGuidGenerator](Guid-Generation.md) service. It doesn't use auto object mapping here. +* If the client wants to **assign this issue to a user** on object creation, it uses the `IssueManager` to do it by allowing the `IssueManager` to perform the necessary checks before this assignment. +* **Saves** the entity to the database. +* Finally uses the `IObjectMapper` to return an `IssueDto` that is automatically created by **mapping** from the new `Issue` entity. + +#### Applying Domain Rules on Entity Creation + +The example `Issue` entity has no business rule on entity creation, except some formal validations in the constructor. However, there maybe scenarios where entity creation should check some extra business rules. + +For example, assume that you **don't want** to allow to create an issue if there is already an issue with **exactly the same `Title`**. Where to implement this rule? It is **not proper** to implement this rule in the **Application Service**, because it is a **core business (domain) rule** that should always be checked. + +This rule should be implemented in a **Domain Service**, `IssueManager` in this case. So, we need to force the Application Layer always to use the `IssueManager` to create a new `Issue.` + +First, we can make the `Issue` constructor `internal`, instead of `public`: + +````csharp +public class Issue : AggregateRoot +{ + //... + + internal Issue( + Guid id, + Guid repositoryId, + string title, + string text = null + ) : base(id) + { + RepositoryId = repositoryId; + Title = Check.NotNullOrWhiteSpace(title, nameof(title)); + Text = text; //Allow empty/null + } + + //... +} +```` + +This prevents Application Services to directly use the constructor, so they will use the `IssueManager`. Then we can add a `CreateAsync` method to the `IssueManager`: + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; + +namespace IssueTracking.Issues +{ + public class IssueManager : DomainService + { + private readonly IRepository _issueRepository; + + public IssueManager(IRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task CreateAsync( + Guid repositoryId, + string title, + string text = null) + { + if (await _issueRepository.AnyAsync(i => i.Title == title)) + { + throw new BusinessException("IssueTracking:IssueWithSameTitleExists"); + } + + return new Issue( + GuidGenerator.Create(), + repositoryId, + title, + text + ); + } + } +} +```` + +* `CreateAsync` method checks if there is already an issue with the same title and throws a business exception in this case. +* If there is no duplication, it creates and returns a new `Issue`. + +The `IssueAppService` is changed as shown below in order to use the `IssueManager`'s `CreateAsync` method: + +````csharp +public class IssueAppService : ApplicationService, IIssueAppService +{ + private readonly IssueManager _issueManager; + private readonly IRepository _issueRepository; + private readonly IRepository _userRepository; + + public IssueAppService( + IssueManager issueManager, + IRepository issueRepository, + IRepository userRepository) + { + _issueManager = issueManager; + _issueRepository = issueRepository; + _userRepository = userRepository; + } + + public async Task CreateAsync(IssueCreationDto input) + { + // Create a valid entity using the IssueManager + var issue = await _issueManager.CreateAsync( + input.RepositoryId, + input.Title, + input.Text + ); + + // Apply additional domain actions + if (input.AssignedUserId.HasValue) + { + var user = await _userRepository.GetAsync(input.AssignedUserId.Value); + await _issueManager.AssignToAsync(issue, user); + } + + // Save + await _issueRepository.InsertAsync(issue); + + // Return a DTO represents the new Issue + return ObjectMapper.Map(issue); + } +} + +// *** IssueCreationDto class *** +public class IssueCreationDto +{ + public Guid RepositoryId { get; set; } + [Required] + public string Title { get; set; } + public Guid? AssignedUserId { get; set; } + public string Text { get; set; } +} +```` + +##### Discussion: Why is the Issue not saved to the database in `IssueManager`? + +You may ask "**Why didn't `IssueManager` save the `Issue` to the database?**". We think it is the responsibility of the Application Service. + +Because, the Application Service may require additional changes/operations on the `Issue` object before saving it. If Domain Service saves it, then the *Save* operation is duplicated; + +* It causes performance lost because of double database round trip. +* It requires explicit database transaction that covers both operations. +* If additional actions cancel the entity creation because of a business rule, the transaction should be rolled back in the database. + +When you check the `IssueAppService`, you will see the advantage of **not saving** `Issue` to the database in the `IssueManager.CreateAsync`. Otherwise, we would need to perform one *Insert* (in the `IssueManager`) and one *Update* (after the Assignment). + +##### Discussion: Why is the duplicate Title check not implemented in the Application Service? + +We could simple say "Because it is a **core domain logic** and should be implemented in the Domain Layer". However, it brings a new question "**How did you decide** that it is a core domain logic, but not an application logic?" (we will discuss the difference later with more details). + +For this example, a simple question can help us to make the decision: "If we have another way (use case) of creating an issue, should we still apply the same rule? Is that rule should *always* be implemented". You may think "Why do we have a second way of creating an issue?". However, in real life, you have; + +* **End users** of the application may create issues in your application's standard UI. +* You may have a second **back office** application that is used by your own employees and you may want to provide a way of creating issues (probably with different authorization rules in this case). +* You may have an HTTP API that is open to **3rd-party clients** and they create issues. +* You may have a **background worker** service that do something and creates issues if it detects some problems. In this way, it will create an issue without any user interaction (and probably without any standard authorization check). +* You may have a button on the UI that **converts** something (for example, a discussion) to an issue. + +We can give more examples. All of these are should be implemented by **different Application Service methods** (see the *Multiple Application Layers* section below), but they **always** follow the rule: Title of the new issue can not be same of any existing issue! That's why this logic is a **core domain logic**, should be located in the Domain Layer and **should not be duplicated** in all these application service methods. + +### Updating / Manipulating An Entity + +Once an entity is created, it is updated/manipulated by the use cases until it is deleted from the system. There can be different types of the use cases directly or indirectly changes an entity. + +In this section, we will discuss a typical update operation that changes multiple properties of an `Issue`. + +This time, beginning from the *Update* DTO: + +````csharp +public class UpdateIssueDto +{ + [Required] + public string Title { get; set; } + public string Text { get; set; } + public Guid? AssignedUserId { get; set; } +} +```` + +By comparing to `IssueCreationDto`, you see no `RepositoryId`. Because, our system doesn't allow to move issues across repositories (think as GitHub repositories). Only `Title` is required and the other properties are optional. + +Let's see the *Update* implementation in the `IssueAppService`: + +````csharp +public class IssueAppService : ApplicationService, IIssueAppService +{ + private readonly IssueManager _issueManager; + private readonly IRepository _issueRepository; + private readonly IRepository _userRepository; + + public IssueAppService( + IssueManager issueManager, + IRepository issueRepository, + IRepository userRepository) + { + _issueManager = issueManager; + _issueRepository = issueRepository; + _userRepository = userRepository; + } + + public async Task UpdateAsync(Guid id, UpdateIssueDto input) + { + // Get entity from database + var issue = await _issueRepository.GetAsync(id); + + // Change Title + await _issueManager.ChangeTitleAsync(issue, input.Title); + + // Change Assigned User + if (input.AssignedUserId.HasValue) + { + var user = await _userRepository.GetAsync(input.AssignedUserId.Value); + await _issueManager.AssignToAsync(issue, user); + } + + // Change Text (no business rule, all values accepted) + issue.Text = input.Text; + + // Update entity in the database + await _issueRepository.UpdateAsync(issue); + + // Return a DTO represents the new Issue + return ObjectMapper.Map(issue); + } +} +```` + +* `UpdateAsync` method gets `id` as a separate parameter. It is not included in the `UpdateIssueDto`. This is a design decision that helps ABP to properly define HTTP routes when you [auto expose](API/Auto-API-Controllers.md) this service as an HTTP API endpoint. So, that's not related to DDD. +* It starts by **getting** the `Issue` entity **from the database**. +* Uses `IssueManager`'s `ChangeTitleAsync` instead of directly calling `Issue.SetTitle(...)`. Because we need to implement the **duplicate Title check** as just done in the *Entity Creation*. This requires some changes in the `Issue` and `IssueManager` classes (will be explained below). +* Uses `IssueManager`'s `AssignToAsync` method if the **assigned user** is being changed with this request. +* Directly sets the `Issue.Text` since there is **no business rule** for that. If we need later, we can always refactor. +* **Saves changes** to the database. Again, saving changed entities is a responsibility of the Application Service method that coordinates the business objects and the transaction. If the `IssueManager` had saved internally in `ChangeTitleAsync` and `AssignToAsync` method, there would be double database operation (see the *Discussion: Why is the Issue not saved to the database in `IssueManager`?* above). +* Finally uses the `IObjectMapper` to return an `IssueDto` that is automatically created by **mapping** from the updated `Issue` entity. + +As said, we need some changes in the `Issue` and `IssueManager` classes. + +First, made `SetTitle` internal in the `Issue` class: + +````csharp +internal void SetTitle(string title) +{ + Title = Check.NotNullOrWhiteSpace(title, nameof(title)); +} +```` + +Then added a new method to the `IssueManager` to change the Title: + +````csharp +public async Task ChangeTitleAsync(Issue issue, string title) +{ + if (issue.Title == title) + { + return; + } + + if (await _issueRepository.AnyAsync(i => i.Title == title)) + { + throw new BusinessException("IssueTracking:IssueWithSameTitleExists"); + } + + issue.SetTitle(title); +} +```` + +## Domain Logic & Application Logic + +As mentioned before, *Business Logic* in the Domain Driven Design is spitted into two parts (layers): *Domain Logic* and *Application Logic*: + +![domain-driven-design-domain-vs-application-logic](images/domain-driven-design-domain-vs-application-logic.png) + +Domain Logic consists of the *Core Domain Rules* of the system while Application Logic implements application specific *Use Cases*. + +While the definition is clear, the implementation may not be easy. You may be undecided which code should stand in the Application Layer, which code should be in the Domain Layer. This section tries to explain the differences. + +### Multiple Application Layers + +DDD helps to **deal with complexity** when your system is large. Especially, if there are **multiple applications** are being developed in a **single domain,** then the **Domain Logic vs Application Logic separation** becomes much more important. + +Assume that you are building a system that has multiple applications; + +* A **Public Web Site Application**, built with ASP.NET Core MVC, to show your products to users. Such a web site doesn't require authentication to see the products. The users login to the web site, only if they are performing some actions (like adding a product to the basket). +* A **Back Office Application**, built with Angular UI (that uses REST APIs). This application used by office workers of the company to manage the system (like editing product descriptions). +* A **Mobile Application** that has much simpler UI compared to the Public Web Site. It may communicate to the server via REST APIs or another technology (like TCP sockets). + +![domain-driven-design-multiple-applications](images/domain-driven-design-multiple-applications.png) + +Every application will have different **requirements**, different **use cases** (Application Service methods), different **DTOs**, different **validation** and **authorization** rules... etc. + +Mixing all these logics into a single application layer makes your services contain too many `if` conditions with **complicated business logic** makes your code **harder to develop, maintain and test** and leads to potential bugs. + +If you've multiple applications with a single domain; + +* Create **separate application layers** for each application/client type and implement application specific business logic in these separate layers. +* Use a **single domain layer** to share the core domain logic. + +Such a design makes it even more important to distinguish between Domain logic and Application Logic. + +To be more clear about the implementation, you can create different projects (`.csproj`) for each application types. For example; + +* `IssueTracker.Admin.Application` & `IssueTracker.Admin.Application.Contacts` projects for the Back Office (admin) Application. +* `IssueTracker.Public.Application` & `IssueTracker.Public.Application.Contracts` projects for the Public Web Application. +* `IssueTracker.Mobile.Application` & `IssueTracker.Mobile.Application.Contracts` projects for the Mobile Application. + +### Examples + +This section contains some Application Service and Domain Service examples to discuss how to decide to place business logic inside these services. + +**Example: Creating a new `Organization` in a Domain Service** + +````csharp +public class OrganizationManager : DomainService +{ + private readonly IRepository _organizationRepository; + private readonly ICurrentUser _currentUser; + private readonly IAuthorizationService _authorizationService; + private readonly IEmailSender _emailSender; + + public OrganizationManager( + IRepository organizationRepository, + ICurrentUser currentUser, + IAuthorizationService authorizationService, + IEmailSender emailSender) + { + _organizationRepository = organizationRepository; + _currentUser = currentUser; + _authorizationService = authorizationService; + _emailSender = emailSender; + } + + public async Task CreateAsync(string name) + { + if (await _organizationRepository.AnyAsync(x => x.Name == name)) + { + throw new BusinessException("IssueTracking:DuplicateOrganizationName"); + } + + await _authorizationService.CheckAsync("OrganizationCreationPermission"); + + Logger.LogDebug($"Creating organization {name} by {_currentUser.UserName}"); + + var organization = new Organization(); + + await _emailSender.SendAsync( + "systemadmin@issuetracking.com", + "New Organization", + "A new organization created with name: " + name + ); + + return organization; + } +} +```` + +Let's see the `CreateAsync` method step by step to discuss if the code part should be in the Domain Service, or not; + +* **CORRECT**: It first checks for **duplicate organization name** and and throws exception in this case. This is something related to core domain rule and we never allow duplicated names. +* **WRONG**: Domain Services should not perform **authorization**. [Authorization](Authorization.md) should be done in the Application Layer. +* **WRONG**: It logs a message with including the [Current User](CurrentUser.md)'s `UserName`. Domain service should not be depend on the Current User. Domain Services should be usable even if there is no user in the system. Current User (Session) should be a Presentation/Application Layer related concept. +* **WRONG**: It sends an [email](Emailing.md) about this new organization creation. We think this is also a use case specific business logic. You may want to create different type of emails in different use cases or don't need to send emails in some cases. + +**Example: Creating a new `Organization` in an Application Service** + +````csharp +public class OrganizationAppService : ApplicationService +{ + private readonly OrganizationManager _organizationManager; + private readonly IPaymentService _paymentService; + private readonly IEmailSender _emailSender; + + public OrganizationAppService( + OrganizationManager organizationManager, + IPaymentService paymentService, + IEmailSender emailSender) + { + _organizationManager = organizationManager; + _paymentService = paymentService; + _emailSender = emailSender; + } + + [UnitOfWork] + [Authorize("OrganizationCreationPermission")] + public async Task CreateAsync(CreateOrganizationDto input) + { + await _paymentService.ChargeAsync( + CurrentUser.Id, + GetOrganizationPrice() + ); + + var organization = await _organizationManager.CreateAsync(input.Name); + + await _organizationManager.InsertAsync(organization); + + await _emailSender.SendAsync( + "systemadmin@issuetracking.com", + "New Organization", + "A new organization created with name: " + input.Name + ); + + return organization; // !!! + } + + private double GetOrganizationPrice() + { + return 42; //Gets from somewhere else... + } +} +```` + +Let's see the `CreateAsync` method step by step to discuss if the code part should be in the Application Service, or not; + +* **CORRECT**: Application Service methods should be unit of work (transactional). ABP's [Unit Of Work](Unit-Of-Work.md) system makes this automatic (even without need to add `[UnitOfWork]` attribute for the Application Services). +* **CORRECT**: [Authorization](Authorization.md) should be done in the application layer. Here, it is done by using the `[Authorize]` attribute. +* **CORRECT**: Payment (an infrastructure service) is called to charge money for this operation (Creating an Organization is a paid service in our business). +* **CORRECT**: Application Service method is responsible to save changes to the database. +* **CORRECT**: We can send [email](Emailing.md) as a notification to the system admin. +* **WRONG**: Do not return entities from the Application Services. Return a DTO instead. + +**Discussion: Why don't we move the payment logic into the domain service?** + +You may wonder why the payment code is not inside the `OrganizationManager`. It is an **important thing** and we never want to **miss the payment**. + +However, **being important is not sufficient** to consider a code as a Core Business Logic. We may have **other use cases** where we don't charge money to create a new Organization. Examples; + +* An admin user can use a Back Office Application to create a new organization without any payment. +* A background-working data import/integration/synchronization system may also need to create organizations without any payment operation. + +As you see, **payment is not a necessary operation to create a valid organization**. It is a use-case specific application logic. + +**Example: CRUD Operations** + +````csharp +public class IssueAppService +{ + private readonly IssueManager _issueManager; + + public IssueAppService(IssueManager issueManager) + { + _issueManager = issueManager; + } + + public async Task GetAsync(Guid id) + { + return await _issueManager.GetAsync(id); + } + + public async Task CreateAsync(IssueCreationDto input) + { + await _issueManager.CreateAsync(input); + } + + public async Task UpdateAsync(UpdateIssueDto input) + { + await _issueManager.UpdateAsync(input); + } + + public async Task DeleteAsync(Guid id) + { + await _issueManager.DeleteAsync(id); + } +} +```` + +This Application Service **does nothing** itself and **delegates all the work** to the *Domain Service*. It even passes the DTOs to the `IssueManager`. + +* **Do not** create Domain Service methods just for simple **CRUD** operations **without any domain logic**. +* **Never** pass **DTOs** to or return **DTOs** from the Domain Services. + +Application Services can directly work with repositories to query, create, update or delete data unless there are some domain logics should be performed during these operations. In such cases, create Domain Service methods, but only for those really necessary. + +> Do not create such CRUD domain service methods just by thinking that they may be needed in the future ([YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it))! Do it when you need and refactor the existing code. Since the Application Layer gracefully abstracts the Domain Layer, the refactoring process doesn't effect the UI Layer and other clients. + +## Reference Books + +If you are more interested in the Domain Driven Design and building large-scale enterprise systems, the following books are recommended as reference books; + +* "*Domain Driven Design*" by Eric Evans +* "*Implementing Domain Driven Design*" by Vaughn Vernon +* "*Clean Architecture*" by Robert C. Martin diff --git a/docs/en/Domain-Driven-Design.md b/docs/en/Domain-Driven-Design.md index ce33295492..f18c1882ef 100644 --- a/docs/en/Domain-Driven-Design.md +++ b/docs/en/Domain-Driven-Design.md @@ -10,7 +10,7 @@ ABP framework provides an **infrastructure** to make **Domain Driven Design** ba > - Basing complex designs on a model of the domain; > - Initiating a creative collaboration between technical and domain experts to iteratively refine a conceptual model that addresses particular domain problems. -### Layers +## Layers & Building Blocks ABP follows DDD principles and patterns to achieve a layered application model which consists of four fundamental layers: @@ -19,15 +19,19 @@ ABP follows DDD principles and patterns to achieve a layered application model w - **Domain Layer**: Includes business objects and the core (domain) business rules. This is the heart of the application. - **Infrastructure Layer**: Provides generic technical capabilities that support higher layers mostly using 3rd-party libraries. -## Contents +DDD mostly interest in the **Domain** and the **Application** layers, rather than the Infrastructure and the Presentation layers. The following documents explains the **infrastructure** provided by the ABP Framework to implement **Building Blocks** of the DDD: * **Domain Layer** * [Entities & Aggregate Roots](Entities.md) - * Value Objects * [Repositories](Repositories.md) - * Domain Services - * Specifications + * [Domain Services](Domain-Services.md) + * [Value Objects](Value-Objects.md) + * [Specifications](Specifications.md) * **Application Layer** * [Application Services](Application-Services.md) * [Data Transfer Objects (DTOs)](Data-Transfer-Objects.md) - * Unit of Work \ No newline at end of file + * [Unit of Work](Unit-Of-Work.md) + +## The Ultimate DDD Implementation Guide + +See the [Implementing Domain Driven Design](Domain-Driven-Design-Implementation-Guide.md) guide as a **complete reference**. The Guide explains the Domain Driven Design and introduces explicit **rules and examples** to give a deep understanding of the **implementation details**. \ No newline at end of file diff --git a/docs/en/Domain-Services.md b/docs/en/Domain-Services.md index 7eb6c917cd..5a4f6393e1 100644 --- a/docs/en/Domain-Services.md +++ b/docs/en/Domain-Services.md @@ -1,3 +1,135 @@ -# ABP Documentation +# Domain Services -TODO! \ No newline at end of file +## Introduction + +In a [Domain Driven Design](Domain-Driven-Design.md) (DDD) solution, the core business logic is generally implemented in aggregates ([entities](Entities.md)) and the Domain Services. Creating a Domain Service is especially needed when; + +* You implement a core domain logic that depends on some services (like repositories or other external services). +* The logic you need to implement is related to more than one aggregate/entity, so it doesn't properly fit in any of the aggregates. + +## ABP Domain Service Infrastructure + +Domain Services are simple, stateless classes. While you don't have to derive from any service or interface, ABP Framework provides some useful base classes and conventions. + +### DomainService & IDomainService + +Either derive a Domain Service from the `DomainService` base class or directly implement the `IDomainService` interface. + +**Example: Create a Domain Service deriving from the `DomainService` base class.** + +````csharp +using Volo.Abp.Domain.Services; + +namespace MyProject.Issues +{ + public class IssueManager : DomainService + { + + } +} +```` + +When you do that; + +* ABP Framework automatically registers the class to the Dependency Injection system with a Transient lifetime. +* You can directly use some common services as base properties, without needing to manually inject (e.g. [ILogger](Logging.md) and [IGuidGenerator](Guid-Generation.md)). + +> It is suggested to name a Domain Service with a `Manager` or `Service` suffix. We typically use the `Manager` suffix as used in the sample above. + +**Example: Implement the domain logic of assigning an Issue to a User** + +````csharp +public class IssueManager : DomainService +{ + private readonly IRepository _issueRepository; + + public IssueManager(IRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task AssignAsync(Issue issue, AppUser user) + { + var currentIssueCount = await _issueRepository + .CountAsync(i => i.AssignedUserId == user.Id); + + //Implementing a core business validation + if (currentIssueCount >= 3) + { + throw new IssueAssignmentException(user.UserName); + } + + issue.AssignedUserId = user.Id; + } +} +```` + +Issue is an [aggregate root](Entities.md) defined as shown below: + +````csharp +public class Issue : AggregateRoot +{ + public Guid? AssignedUserId { get; internal set; } + + //... +} +```` + +* Making the setter `internal` ensures that it can not directly set in the upper layers and forces to always use the `IssueManager` to assign an `Issue` to a `User`. + +### Using a Domain Service + +A Domain Service is typically used in an [application service](Application-Services.md). + +**Example: Use the `IssueManager` to assign an Issue to a User** + +````csharp +using System; +using System.Threading.Tasks; +using MyProject.Users; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace MyProject.Issues +{ + public class IssueAppService : ApplicationService, IIssueAppService + { + private readonly IssueManager _issueManager; + private readonly IRepository _userRepository; + private readonly IRepository _issueRepository; + + public IssueAppService( + IssueManager issueManager, + IRepository userRepository, + IRepository issueRepository) + { + _issueManager = issueManager; + _userRepository = userRepository; + _issueRepository = issueRepository; + } + + public async Task AssignAsync(Guid id, Guid userId) + { + var issue = await _issueRepository.GetAsync(id); + var user = await _userRepository.GetAsync(userId); + + await _issueManager.AssignAsync(issue, user); + await _issueRepository.UpdateAsync(issue); + } + } +} +```` + +Since the `IssueAppService` is in the Application Layer, it can't directly assign an issue to a user. So, it uses the `IssueManager`. + +## Application Services vs Domain Services + +While both of [Application Services](Application-Services.md) and Domain Services implement the business rules, there are fundamental logical and formal differences; + +* Application Services implement the **use cases** of the application (user interactions in a typical web application), while Domain Services implement the **core, use case independent domain logic**. +* Application Services get/return [Data Transfer Objects](Data-Transfer-Objects.md), Domain Service methods typically get and return the **domain objects** ([entities](Entities.md), [value objects](Value-Objects.md)). +* Domain services are typically used by the Application Services or other Domain Services, while Application Services are used by the Presentation Layer or Client Applications. + +## Lifetime + +Lifetime of Domain Services are [transient](https://docs.abp.io/en/abp/latest/Dependency-Injection) and they are automatically registered to the dependency injection system. \ No newline at end of file diff --git a/docs/en/Emailing.md b/docs/en/Emailing.md index aa341a3898..a98874165d 100644 --- a/docs/en/Emailing.md +++ b/docs/en/Emailing.md @@ -223,7 +223,7 @@ Pathes of the templates in the virtual file system are shown below: * `/Volo/Abp/Emailing/Templates/Layout.tpl` * `/Volo/Abp/Emailing/Templates/Message.tpl` -If you add files to the same localization in the virtual file system, your files will override them. +If you add files to the same location in the virtual file system, your files will override them. Templates are inline localized, that means you can take the power of the [localization system](Localization.md) to make your templates multi-cultural. @@ -247,4 +247,4 @@ So, don't confuse if you don't receive emails on DEBUG mode. Emails will be sent ## See Also -* [MailKit integration for sending emails](MailKit.md) \ No newline at end of file +* [MailKit integration for sending emails](MailKit.md) diff --git a/docs/en/Entity-Framework-Core-Migrations.md b/docs/en/Entity-Framework-Core-Migrations.md index a93ee217da..be5968a9af 100644 --- a/docs/en/Entity-Framework-Core-Migrations.md +++ b/docs/en/Entity-Framework-Core-Migrations.md @@ -586,7 +586,7 @@ First step is to change the connection string section inside all the `appsetting ````json "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" } ```` @@ -594,10 +594,10 @@ Change it as shown below: ````json "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true", - "AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true", - "AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true", - "AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True", + "AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True", + "AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True", + "AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True" } ```` diff --git a/docs/en/Entity-Framework-Core-MySQL.md b/docs/en/Entity-Framework-Core-MySQL.md index a04d6073fc..14f419557e 100644 --- a/docs/en/Entity-Framework-Core-MySQL.md +++ b/docs/en/Entity-Framework-Core-MySQL.md @@ -12,10 +12,10 @@ Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFra ## UseMySQL() -Find `UseSqlServer()` calls in your solution, replace with `UseMySQL()`. Check the following files: +Find `UseSqlServer()` calls in your solution. Check the following files: -* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. -* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project. +* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. Replace `UseSqlServer()` with `UseMySQL()`. +* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project. Replace `UseSqlServer()` with `UseMySql()`. Then add a new parameter (`ServerVersion`) to `UseMySql()` method. Example: `.UseMySql(configuration.GetConnectionString("Default"), ServerVersion.FromString("8.0.21-mysql"))`. See [this issue](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/pull/1233) for more information about `ServerVersion`) > Depending on your solution structure, you may find more code files need to be changed. @@ -38,4 +38,4 @@ Run the `.DbMigrator` project to create the database and seed the initial data. ## Run the Application -It is ready. Just run the application and enjoy coding. \ No newline at end of file +It is ready. Just run the application and enjoy coding. diff --git a/docs/en/Entity-Framework-Core.md b/docs/en/Entity-Framework-Core.md index 98163e7c28..b2b216397a 100644 --- a/docs/en/Entity-Framework-Core.md +++ b/docs/en/Entity-Framework-Core.md @@ -92,7 +92,7 @@ protected override void OnModelCreating(ModelBuilder builder) b.ToTable("Books"); //Configure the base properties - b.ConfigureByConvention(); + b.ConfigureByConvention(); //Configure other properties (if you are using the fluent API) b.Property(x => x.Name).IsRequired().HasMaxLength(128); @@ -113,7 +113,7 @@ If you have multiple databases in your application, you can configure the connec [ConnectionStringName("MySecondConnString")] public class MyDbContext : AbpDbContext { - + } ``` @@ -174,7 +174,7 @@ public class Book : AggregateRoot } ``` -(`BookType` is a simple enum here) And you want to create a new `Book` entity in a [domain service](Domain-Services.md): +(`BookType` is a simple `enum` here and not important) And you want to create a new `Book` entity in a [domain service](Domain-Services.md): ````csharp public class BookManager : DomainService @@ -221,12 +221,13 @@ public interface IBookRepository : IRepository } ```` -You generally want to derive from the `IRepository` to inherit standard repository methods. However, you don't have to. Repository interfaces are defined in the domain layer of a layered application. They are implemented in the data/infrastructure layer (`EntityFrameworkCore` project in a [startup template](https://abp.io/Templates)). +You generally want to derive from the `IRepository` to inherit standard repository methods (while, you don't have to do). Repository interfaces are defined in the domain layer of a layered application. They are implemented in the data/infrastructure layer (`EntityFrameworkCore` project in a [startup template](https://abp.io/Templates)). Example implementation of the `IBookRepository` interface: ````csharp -public class BookRepository : EfCoreRepository, IBookRepository +public class BookRepository + : EfCoreRepository, IBookRepository { public BookRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) @@ -254,7 +255,7 @@ If you want to replace default repository implementation with your custom reposi context.Services.AddAbpDbContext(options => { options.AddDefaultRepositories(); - + //Replaces IRepository options.AddRepository(); }); @@ -263,7 +264,7 @@ context.Services.AddAbpDbContext(options => This is especially important when you want to **override a base repository method** to customize it. For instance, you may want to override `DeleteAsync` method to delete a specific entity in a more efficient way: ````csharp -public override async Task DeleteAsync( +public async override Task DeleteAsync( Guid id, bool autoSave = false, CancellationToken cancellationToken = default) @@ -272,6 +273,278 @@ public override async Task DeleteAsync( } ```` +## Loading Related Entities + +Assume that you've an `Order` with a collection of `OrderLine`s and the `OrderLine` has a navigation property to the `Order`: + +````csharp +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Volo.Abp.Auditing; +using Volo.Abp.Domain.Entities; + +namespace MyCrm +{ + public class Order : AggregateRoot, IHasCreationTime + { + public Guid CustomerId { get; set; } + public DateTime CreationTime { get; set; } + + public ICollection Lines { get; set; } //Sub collection + + public Order() + { + Lines = new Collection(); + } + } + + public class OrderLine : Entity + { + public Order Order { get; set; } //Navigation property + public Guid OrderId { get; set; } + + public Guid ProductId { get; set; } + public int Count { get; set; } + public double UnitPrice { get; set; } + } +} + +```` + +And defined the database mapping as shown below: + +````csharp +builder.Entity(b => +{ + b.ToTable("Orders"); + b.ConfigureByConvention(); + + //Define the relation + b.HasMany(x => x.Lines) + .WithOne(x => x.Order) + .HasForeignKey(x => x.OrderId) + .IsRequired(); +}); + +builder.Entity(b => +{ + b.ToTable("OrderLines"); + b.ConfigureByConvention(); +}); +```` + +When you query an `Order`, you may want to **include** all the `OrderLine`s in a single query or you may want to **load them later** on demand. + +> Actually these are not directly related to the ABP Framework. You can follow the [EF Core documentation](https://docs.microsoft.com/en-us/ef/core/querying/related-data/) to learn all the details. This section will cover some topics related to the ABP Framework. + +### Eager Loading / Load With Details + +You have different options when you want to load the related entities while querying an entity. + +#### Repository.WithDetails + +`IRepository.WithDetails(...)` can be used to include one relation collection/property to the query. + +**Example: Get an order with lines** + +````csharp +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; + +namespace MyCrm +{ + public class OrderManager : DomainService + { + private readonly IRepository _orderRepository; + + public OrderManager(IRepository orderRepository) + { + _orderRepository = orderRepository; + } + + public async Task TestWithDetails(Guid id) + { + var query = _orderRepository + .WithDetails(x => x.Lines) + .Where(x => x.Id == id); + + var order = await AsyncExecuter.FirstOrDefaultAsync(query); + } + } +} +```` + +> `AsyncExecuter` is used to execute async LINQ extensions without depending on the EF Core. If you add EF Core NuGet package reference to your project, then you can directly use `await _orderRepository.WithDetails(x => x.Lines).FirstOrDefaultAsync()`. But, this time you depend on the EF Core in your domain layer. See the [repository document](Repositories.md) to learn more. + +**Example: Get a list of orders with their lines** + +````csharp +public async Task TestWithDetails() +{ + var query = _orderRepository + .WithDetails(x => x.Lines); + + var orders = await AsyncExecuter.ToListAsync(query); +} +```` + +> `WithDetails` method can get more than one expression parameter if you need to include more than one navigation property or collection. + +#### DefaultWithDetailsFunc + +If you don't pass any expression to the `WithDetails` method, then it includes all the details using the `DefaultWithDetailsFunc` option you provide. + +You can configure `DefaultWithDetailsFunc` for an entity in the `ConfigureServices` method of your [module](Module-Development-Basics.md) in your `EntityFrameworkCore` project. + +**Example: Include `Lines` while querying an `Order`** + +````csharp +Configure(options => +{ + options.Entity(orderOptions => + { + orderOptions.DefaultWithDetailsFunc = query => query.Include(o => o.Lines); + }); +}); +```` + +> You can fully use the EF Core API here since this is located in the EF Core integration project. + +Then you can use the `WithDetails` without any parameter: + +````csharp +public async Task TestWithDetails() +{ + var query = _orderRepository.WithDetails(); + var orders = await AsyncExecuter.ToListAsync(query); +} +```` + +`WithDetails()` executes the expression you've setup as the `DefaultWithDetailsFunc`. + +#### Repository Get/Find Methods + +Some of the standard [Repository](Repositories.md) methods have optional `includeDetails` parameters; + +* `GetAsync` and `FindAsync` gets `includeDetails` with default value is `true`. +* `GetListAsync` and `GetPagedListAsync` gets `includeDetails` with default value is `false`. + +That means, the methods return a **single entity includes details** by default while list returning methods don't include details by default. You can explicitly pass `includeDetails` to change the behavior. + +> These methods use the `DefaultWithDetailsFunc` option that is explained above. + +**Example: Get an order with details** + +````csharp +public async Task TestWithDetails(Guid id) +{ + var order = await _orderRepository.GetAsync(id); +} +```` + +**Example: Get an order without details** + +````csharp +public async Task TestWithoutDetails(Guid id) +{ + var order = await _orderRepository.GetAsync(id, includeDetails: false); +} +```` + +**Example: Get list of entities with details** + +````csharp +public async Task TestWithDetails() +{ + var orders = await _orderRepository.GetListAsync(includeDetails: true); +} +```` + +#### Alternatives + +The repository patters tries to encapsulate the EF Core, so your options are limited. If you need an advanced scenario, you can follow one of the options; + +* Create a custom repository method and use the complete EF Core API. +* Reference to the `Volo.Abp.EntityFrameworkCore` package from your project. In this way, you can directly use `Include` and `ThenInclude` in your code. + +See also [eager loading document](https://docs.microsoft.com/en-us/ef/core/querying/related-data/eager) of the EF Core. + +### Explicit / Lazy Loading + +If you don't include relations while querying an entity and later need to access to a navigation property or collection, you have different options. + +#### EnsurePropertyLoadedAsync / EnsureCollectionLoadedAsync + +Repositories provide `EnsurePropertyLoadedAsync` and `EnsureCollectionLoadedAsync` extension methods to **explicitly load** a navigation property or sub collection. + +**Example: Load Lines of an Order when needed** + +````csharp +public async Task TestWithDetails(Guid id) +{ + var order = await _orderRepository.GetAsync(id, includeDetails: false); + //order.Lines is empty on this stage + + await _orderRepository.EnsureCollectionLoadedAsync(order, x => x.Lines); + //order.Lines is filled now +} +```` + +`EnsurePropertyLoadedAsync` and `EnsureCollectionLoadedAsync` methods do nothing if the property or collection was already loaded. So, calling multiple times has no problem. + +See also [explicit loading document](https://docs.microsoft.com/en-us/ef/core/querying/related-data/explicit) of the EF Core. + +#### Lazy Loading with Proxies + +Explicit loading may not be possible in some cases, especially when you don't have a reference to the `Repository` or `DbContext`. Lazy Loading is a feature of the EF Core that loads the related properties / collections when you first access to it. + +To enable lazy loading; + +1. Install the [Microsoft.EntityFrameworkCore.Proxies](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Proxies/) package into your project (typically to the EF Core integration project) +2. Configure `UseLazyLoadingProxies` for your `DbContext` (in the `ConfigureServices` method of your module in your EF Core project). Example: + +````csharp +Configure(options => +{ + options.PreConfigure(opts => + { + opts.DbContextOptions.UseLazyLoadingProxies(); //Enable lazy loading + }); + + options.UseSqlServer(); +}); +```` + +3. Make your navigation properties and collections `virtual`. Examples: + +````csharp +public virtual ICollection Lines { get; set; } //virtual collection +public virtual Order Order { get; set; } //virtual navigation property +```` + +Once you enable lazy loading and arrange your entities, you can freely access to the navigation properties and collections: + +````csharp +public async Task TestWithDetails(Guid id) +{ + var order = await _orderRepository.GetAsync(id); + //order.Lines is empty on this stage + + var lines = order.Lines; + //order.Lines is filled (lazy loaded) +} +```` + +Whenever you access to a property/collection, EF Core automatically performs an additional query to load the property/collection from the database. + +> Lazy loading should be carefully used since it may cause performance problems in some specific cases. + +See also [lazy loading document](https://docs.microsoft.com/en-us/ef/core/querying/related-data/lazy) of the EF Core. + ## Access to the EF Core API In most cases, you want to hide EF Core APIs behind a repository (this is the main purpose of the repository pattern). However, if you want to access the `DbContext` instance over the repository, you can use `GetDbContext()` or `GetDbSet()` extension methods. Example: @@ -296,7 +569,7 @@ public class BookService * `GetDbContext` returns a `DbContext` reference instead of `BookStoreDbContext`. You can cast it, however in most cases you don't need it. -> Important: You must reference to the `Volo.Abp.EntityFrameworkCore` package from the project you want to access to the DbContext. This breaks encapsulation, but this is what you want in that case. +> Important: You must reference to the `Volo.Abp.EntityFrameworkCore` package from the project you want to access to the `DbContext`. This breaks encapsulation, but this is what you want in that case. ## Extra Properties & Object Extension Manager @@ -365,7 +638,7 @@ public class MyRepositoryBase : EfCoreRepository where TEntity : class, IEntity { - public MyRepositoryBase(IDbContextProvider dbContextProvider) + public MyRepositoryBase(IDbContextProvider dbContextProvider) : base(dbContextProvider) { } @@ -395,7 +668,7 @@ context.Services.AddAbpDbContext(options => typeof(MyRepositoryBase<,>), typeof(MyRepositoryBase<>) ); - + //... }); ``` @@ -446,6 +719,63 @@ context.Services.AddAbpDbContext(options => In this example, `OtherDbContext` implements `IBookStoreDbContext`. This feature allows you to have multiple DbContext (one per module) on development, but single DbContext (implements all interfaces of all DbContexts) on runtime. +### Split Queries + +ABP enables [split queries](https://docs.microsoft.com/en-us/ef/core/querying/single-split-queries) globally by default for better performance. You can change it as needed. + +**Example** + +````csharp +Configure(options => +{ + options.UseSqlServer(optionsBuilder => + { + optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery); + }); +}); +```` + +### Customize Bulk Operations + +If you have better logic or using an external library for bulk operations, you can override the logic via implementing`IEfCoreBulkOperationProvider`. + +- You may use example template below: + +```csharp +public class MyCustomEfCoreBulkOperationProvider : IEfCoreBulkOperationProvider, ITransientDependency +{ + public async Task DeleteManyAsync(IEfCoreRepository repository, + IEnumerable entities, + bool autoSave, + CancellationToken cancellationToken) + where TDbContext : IEfCoreDbContext + where TEntity : class, IEntity + { + // Your logic here. + } + + public async Task InsertManyAsync(IEfCoreRepository repository, + IEnumerable entities, + bool autoSave, + CancellationToken cancellationToken) + where TDbContext : IEfCoreDbContext + where TEntity : class, IEntity + { + // Your logic here. + } + + public async Task UpdateManyAsync(IEfCoreRepository repository, + IEnumerable entities, + bool autoSave, + CancellationToken cancellationToken) + where TDbContext : IEfCoreDbContext + where TEntity : class, IEntity + { + // Your logic here. + } +} +``` + ## See Also -* [Entities](Entities.md) \ No newline at end of file +* [Entities](Entities.md) diff --git a/docs/en/Exception-Handling.md b/docs/en/Exception-Handling.md index 59a2c6bd0e..8c84157878 100644 --- a/docs/en/Exception-Handling.md +++ b/docs/en/Exception-Handling.md @@ -1,11 +1,11 @@ # Exception Handling -ABP provides a built-in infrastructure and offers a standard model for handling exceptions in a web application. +ABP provides a built-in infrastructure and offers a standard model for handling exceptions. * Automatically **handles all exceptions** and sends a standard **formatted error message** to the client for an API/AJAX request. * Automatically hides **internal infrastructure errors** and returns a standard error message. -* Provides a configurable way to **localize** exception messages. -* Automatically maps standard exceptions to **HTTP status codes** and provides a configurable option to map these to custom exceptions. +* Provides an easy and configurable way to **localize** exception messages. +* Automatically maps standard exceptions to **HTTP status codes** and provides a configurable option to map custom exceptions. ## Automatic Exception Handling @@ -85,7 +85,7 @@ Error **details** in an optional field of the JSON error message. Thrown `Except ### Logging -Caught exceptions are automatically logged. +Caught exceptions are automatically logged. #### Log Level @@ -300,7 +300,7 @@ In this case, create a class derived from the `ExceptionSubscriber` class in you ````csharp public class MyExceptionSubscriber : ExceptionSubscriber { - public override async Task HandleAsync(ExceptionNotificationContext context) + public async override Task HandleAsync(ExceptionNotificationContext context) { //TODO... } diff --git a/docs/en/Getting-Started-AspNetCore-Application.md b/docs/en/Getting-Started-AspNetCore-Application.md index 9698b1dfb0..9202ed29f1 100644 --- a/docs/en/Getting-Started-AspNetCore-Application.md +++ b/docs/en/Getting-Started-AspNetCore-Application.md @@ -4,7 +4,7 @@ This tutorial explains how to start ABP from scratch with minimal dependencies. ## Create A New Project -1. Create a new AspNet Core Web Application from Visual Studio 2019 (16.4.0+): +1. Create a new AspNet Core Web Application from Visual Studio 2019 (16.8.0+): ![](images/create-new-aspnet-core-application-v2.png) diff --git a/docs/en/Getting-Started-Create-Solution.md b/docs/en/Getting-Started-Create-Solution.md new file mode 100644 index 0000000000..75a50a737f --- /dev/null +++ b/docs/en/Getting-Started-Create-Solution.md @@ -0,0 +1,65 @@ +# Getting Started + +````json +//[doc-params] +{ + "UI": ["MVC", "Blazor", "NG"], + "DB": ["EF", "Mongo"], + "Tiered": ["Yes", "No"] +} +```` + +> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. + +## Create a New Project + +Use the `new` command of the ABP CLI to create a new project: + +````shell +abp new Acme.BookStore{{if UI == "NG"}} -u angular{{else if UI == "Blazor"}} -u blazor{{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes"}}{{if UI == "MVC"}} --tiered{{else}} --separate-identity-server{{end}}{{end}} +```` + +*You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.* + +{{ if Tiered == "Yes" }} + +{{ if UI == "MVC" }} + +* `--tiered` argument is used to create N-tiered solution where authentication server, UI and API layers are physically separated. + +{{ else }} + +* `--separate-identity-server` argument is used to separate the identity server application from the API host application. If not specified, you will have a single endpoint on the server. + +{{ end }} + +{{ end }} + +> [ABP CLI document](./CLI.md) covers all of the available commands and options. + +> Alternatively, you can **create and download** projects from [ABP Framework website](https://abp.io/get-started) by easily selecting the all the options from the page. + +### The Solution Structure + +The solution has a layered structure (based on the [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects. See the [application template document](Startup-Templates/Application.md) to understand the solution structure in details. + +{{ if DB == "Mongo" }} + +#### MongoDB Transactions + +The [startup template](Startup-templates/Index.md) **disables** transactions in the `.MongoDB` project by default. If your MongoDB server supports transactions, you can enable the it in the *YourProjectMongoDbModule* class's `ConfigureServices` method: + + ```csharp +Configure(options => +{ + options.TransactionBehavior = UnitOfWorkTransactionBehavior.Auto; +}); + ``` + +> Or you can delete that code since `Auto` is already the default behavior. + +{{ end }} + +## Next Step + +* [Running the solution](Getting-Started-Running-Solution.md) \ No newline at end of file diff --git a/docs/en/Getting-Started-React-Native.md b/docs/en/Getting-Started-React-Native.md index a06257fec9..08faac3608 100644 --- a/docs/en/Getting-Started-React-Native.md +++ b/docs/en/Getting-Started-React-Native.md @@ -27,13 +27,13 @@ You have multiple options to initiate a new React Native project that works with ### 1. Using ABP CLI -ABP CLI is probably the most convenient and flexible way to initiate an ABP solution with a React Native application. Simply [install the ABP CLI](../../CLI.md) and run the following command in your terminal: +ABP CLI is probably the most convenient and flexible way to initiate an ABP solution with a React Native application. Simply [install the ABP CLI](CLI.md) and run the following command in your terminal: ```shell abp new MyCompanyName.MyProjectName -csf -u -m react-native ``` -> To see further options in the CLI, please visit the [CLI manual](../../CLI.md). +> To see further options in the CLI, please visit the [CLI manual](CLI.md). This command will prepare a solution with an **Angular** or an **MVC** (depends on your choice), a **.NET Core**, and a **React Native** project in it. diff --git a/docs/en/Getting-Started-Running-Solution.md b/docs/en/Getting-Started-Running-Solution.md new file mode 100644 index 0000000000..163a3890b2 --- /dev/null +++ b/docs/en/Getting-Started-Running-Solution.md @@ -0,0 +1,205 @@ +# Getting Started + +````json +//[doc-params] +{ + "UI": ["MVC", "Blazor", "NG"], + "DB": ["EF", "Mongo"], + "Tiered": ["Yes", "No"] +} +```` + +> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. + +## Create the Database + +### Connection String + +Check the **connection string** in the `appsettings.json` file under the {{if Tiered == "Yes"}}`.IdentityServer` and `.HttpApi.Host` projects{{else}}{{if UI=="MVC"}}`.Web` project{{else}}`.HttpApi.Host` project{{end}}{{end}} + +{{ if DB == "EF" }} + +````json +"ConnectionStrings": { + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" +} +```` + +> **About the Connection Strings and Database Management Systems** +> +> The solution is configured to use **Entity Framework Core** with **MS SQL Server** by default. However, if you've selected another DBMS using the `-dbms` parameter on the ABP CLI `new` command (like `-dbms MySQL`), the connection string might be different for you. +> +> EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers and you can use any supported DBMS. See [the Entity Framework integration document](Entity-Framework-Core.md) to learn how to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md) if you need later. + +### Database Migrations + +The solution uses the [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). It comes with a `.DbMigrator` console application which **applies the migrations** and also **seeds the initial data**. It is useful on **development** as well as on **production** environment. + +> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. + +### The Initial Migration + +`.DbMigrator` application automatically **creates the Initial migration** on first run. + +**If you are using Visual Studio, you can skip to the *Running the DbMigrator* section.** However, other IDEs (e.g. Rider) may have problems for the first run since it adds the initial migration and compiles the project. In this case, open a command line terminal in the folder of the `.DbMigrator` project and run the following command: + +````bash +dotnet run +```` + +For the next time, you can just run it in your IDE as you normally do. + +### Running the DbMigrator + +Right click to the `.DbMigrator` project and select **Set as StartUp Project** + +![set-as-startup-project](images/set-as-startup-project.png) + + Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: + + ![db-migrator-output](images/db-migrator-output.png) + +> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database (with the password is `1q2w3E*`) which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. + +{{ else if DB == "Mongo" }} + +````json +"ConnectionStrings": { + "Default": "mongodb://localhost:27017/BookStore" +} +```` + +The solution is configured to use **MongoDB** in your local computer, so you need to have a MongoDB server instance up and running or change the connection string to another MongoDB server. + +### Seed Initial Data + +The solution comes with a `.DbMigrator` console application which **seeds the initial data**. It is useful on **development** as well as on **production** environment. + +> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. + +Right click to the `.DbMigrator` project and select **Set as StartUp Project** + +![set-as-startup-project](images/set-as-startup-project.png) + + Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: + + ![db-migrator-output](images/db-migrator-output.png) + +> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database (with the password is `1q2w3E*`) which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. + +{{ end }} + +## Run the Application + +{{ if UI == "MVC" }} + +{{ if Tiered == "Yes" }} + +> Tiered solutions use **Redis** as the distributed cache. Ensure that it is installed and running in your local computer. If you are using a remote Redis Server, set the configuration in the `appsettings.json` files of the projects below. + +1. Ensure that the `.IdentityServer` project is the startup project. Run this application that will open a **login** page in your browser. + +> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. + +You can login, but you cannot enter to the main application here. This is **just the authentication server**. + +2. Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a **Swagger UI** in your browser. + +![swagger-ui](images/swagger-ui.png) + +This is the HTTP API that is used by the web application. + +3. Lastly, ensure that the `.Web` project is the startup project and run the application which will open a **welcome** page in your browser + +![mvc-tiered-app-home](images/bookstore-home.png) + +Click to the **login** button which will redirect you to the *authentication server* to login to the application: + +![bookstore-login](images/bookstore-login.png) + +{{ else # Tiered != "Yes" }} + +Ensure that the `.Web` project is the startup project. Run the application which will open the **login** page in your browser: + +> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. + +![bookstore-login](images/bookstore-login.png) + +{{ end # Tiered }} + +{{ else # UI != "MVC" }} + +### Running the HTTP API Host (Server Side) + +{{ if Tiered == "Yes" }} + +> Tiered solutions use Redis as the distributed cache. Ensure that it is installed and running in your local computer. If you are using a remote Redis Server, set the configuration in the `appsettings.json` files of the projects below. + +Ensure that the `.IdentityServer` project is the startup project. Run the application which will open a **login** page in your browser. + +> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. + +You can login, but you cannot enter to the main application here. This is just the authentication server. + +Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: + +{{ else # Tiered == "No" }} + +Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: + +> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. + +{{ end # Tiered }} + +![swagger-ui](images/swagger-ui.png) + +You can see the application APIs and test them here. Get [more info](https://swagger.io/tools/swagger-ui/) about the Swagger UI. + +{{ end # UI }} + +{{ if UI == "Blazor" }} + +### Running the Blazor Application (Client Side) + +Ensure that the `.Blazor` project is the startup project and run the application. + +> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. + +Once the application starts, click to the **Login** link on to header, which redirects you to the authentication server to enter a username and password: + +![bookstore-login](images/bookstore-login.png) + +{{ else if UI == "NG" }} + +### Running the Angular Application (Client Side) + +Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com/) package manager while `npm install` will also work) + +```bash +yarn +``` + +Once all node modules are loaded, execute `yarn start` (or `npm start`) command: + +```bash +yarn start +``` + +It may take a longer time for the first build. Once it finishes, it opens the Angular UI in your default browser with the [localhost:4200](http://localhost:4200/) address. + +![bookstore-login](images/bookstore-login.png) + +{{ end }} + +Enter **admin** as the username and **1q2w3E*** as the password to login to the application. The application is up and running. You can start developing your application based on this startup template. + +## Mobile Development + +If you want to include a [React Native](https://reactnative.dev/) project in your solution, add `-m react-native` (or `--mobile react-native`) argument to project creation command. This is a basic React Native startup template to develop mobile applications integrated to your ABP based backends. + +See the [Getting Started with the React Native](Getting-Started-React-Native.md) document to learn how to configure and run the React Native application. + +## See Also + +* [Web Application Development Tutorial](Tutorials/Part-1.md) +* [Application Startup Template](Startup-Templates/Application.md) diff --git a/docs/en/Getting-Started-Setup-Environment.md b/docs/en/Getting-Started-Setup-Environment.md new file mode 100644 index 0000000000..39a82076eb --- /dev/null +++ b/docs/en/Getting-Started-Setup-Environment.md @@ -0,0 +1,56 @@ +# Getting Started + +````json +//[doc-params] +{ + "UI": ["MVC", "Blazor", "NG"], + "DB": ["EF", "Mongo"], + "Tiered": ["Yes", "No"] +} +```` + +> This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. + +## Setup Your Development Environment + +First things first! Let's setup your development environment before creating the project. + +### Pre-Requirements + +The following tools should be installed on your development machine: + +* [Visual Studio 2019](https://visualstudio.microsoft.com/vs/) (v16.8+) for Windows / [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). [1](#f-editor) +* [.NET Core 5.0+](https://www.microsoft.com/net/download/dotnet-core/) +{{ if UI != "Blazor" }} +* [Node v12 or v14](https://nodejs.org/) +* [Yarn v1.20+ (not v2)](https://classic.yarnpkg.com/en/docs/install) [2](#f-yarn) or npm v6+ (already installed with Node) +{{ end }} +{{ if Tiered == "Yes" }} +* [Redis](https://redis.io/) (the startup solution uses the Redis as the [distributed cache](Caching.md)). +{{ end }} + +1 _You can use another editor instead of Visual Studio as long as it supports .NET Core and ASP.NET Core._ [↩](#a-editor) + +{{ if UI != "Blazor" }} + +2 _Yarn v2 works differently and is not supported._ [↩](#a-yarn) + +{{ end }} + +### Install the ABP CLI + +[ABP CLI](./CLI.md) is a command line interface that is used to automate some common tasks for ABP based solutions. First, you need to install the ABP CLI using the following command: + +````shell +dotnet tool install -g Volo.Abp.Cli +```` + +If you've already installed, you can update it using the following command: + +````shell +dotnet tool update -g Volo.Abp.Cli +```` + +## Next Step + +* [Creating a new solution](Getting-Started-Create-Solution.md) \ No newline at end of file diff --git a/docs/en/Getting-Started.md b/docs/en/Getting-Started.md index 1e473b266f..ddafff38e4 100644 --- a/docs/en/Getting-Started.md +++ b/docs/en/Getting-Started.md @@ -9,303 +9,12 @@ } ```` -This tutorial explains how to create a new web application using the [application startup template](Startup-Templates/Application.md). - > This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. For other options, please change the preference on top of this document. +## Contents -## Setup Your Development Environment - -First things first! Let's setup your development environment before creating the first project. - -### Pre-Requirements - -The following tools should be installed on your development machine: - -* [Visual Studio 2019](https://visualstudio.microsoft.com/vs/) for Windows / [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/). [1](#f-editor) -* [.NET Core 3.1+](https://www.microsoft.com/net/download/dotnet-core/) - -* [Node v12 or v14](https://nodejs.org/) -* [Yarn v1.20+ (not v2)](https://classic.yarnpkg.com/en/docs/install) [2](#f-yarn) or npm v6+ (already installed with Node) -{{ if Tiered == "Yes" }} - -* [Redis](https://redis.io/) (the startup solution uses the Redis as the [distributed cache](Caching.md)). - -{{ end }} - -1 _You can use another editor instead of Visual Studio as long as it supports .NET Core and ASP.NET Core._ [↩](#a-editor) - -2 _Yarn v2 works differently and is not supported._ [↩](#a-yarn) - -### Install the ABP CLI - -[ABP CLI](./CLI.md) is a command line interface that is used to automate some common tasks for ABP based solutions. - -> ABP CLI is a free & open source tool for the ABP framework. - -First, you need to install the ABP CLI using the following command: - -````shell -dotnet tool install -g Volo.Abp.Cli -```` - -If you've already installed, you can update it using the following command: - -````shell -dotnet tool update -g Volo.Abp.Cli -```` - -## Create a New Project - -Use the `new` command of the ABP CLI to create a new project: - -````shell -abp new Acme.BookStore{{if UI == "NG"}} -u angular{{else if UI == "Blazor"}} -u blazor{{end}}{{if DB == "Mongo"}} -d mongodb{{end}}{{if Tiered == "Yes"}}{{if UI == "MVC"}} --tiered{{else}} --separate-identity-server{{end}}{{end}} -```` - -> You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore. - -> Alternatively, you can select the "Direct Download" tab from the [ABP Framework web site](https://abp.io/get-started) to create a new solution. - -{{ if Tiered == "Yes" }} - -{{ if UI == "MVC" }} - -* `--tiered` argument is used to create N-tiered solution where authentication server, UI and API layers are physically separated. - -{{ else }} - -* `--separate-identity-server` argument is used to separate the identity server application from the API host application. If not specified, you will have a single endpoint on the server. - -{{ end }} - -{{ end }} - -### ABP CLI Commands & Options - -[ABP CLI document](./CLI.md) covers all of the available commands and options for the ABP CLI. This document uses the [application startup template](Startup-Templates/Application.md) to create a new web application. See the [ABP Startup Templates](Startup-Templates/Index.md) document for other templates. - -### The Solution Structure - -The solution has a layered structure (based on the [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects. See the [application template document](Startup-Templates/Application.md) to understand the solution structure in details. - -{{ if DB == "Mongo" }} - -#### MongoDB Transactions - -The [startup template](Startup-templates/Index.md) **disables** transactions in the `.MongoDB` project by default. If your MongoDB server supports transactions, you can enable the it in the *YourProjectMongoDbModule* class: - - ```csharp - Configure(options => - { - options.TransactionBehavior = UnitOfWorkTransactionBehavior.Auto; - }); - ``` - -> Or you can delete this code since this is already the default behavior. - -{{ end }} - -## Create the Database - -### Connection String - -Check the **connection string** in the `appsettings.json` file under the {{if Tiered == "Yes"}}`.IdentityServer` and `.HttpApi.Host` projects{{else}}{{if UI=="MVC"}}`.Web` project{{else}}`.HttpApi.Host` project{{end}}{{end}} - -{{ if DB == "EF" }} - -````json -"ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" -} -```` - -The solution is configured to use **Entity Framework Core** with **MS SQL Server** by default. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use any supported DBMS. See [the Entity Framework integration document](Entity-Framework-Core.md) to learn how to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md). - -### Apply the Migrations - -The solution uses the [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). So, you need to apply migrations to create the database. There are two ways of applying the database migrations. - -#### Apply Migrations Using the DbMigrator - -The solution comes with a `.DbMigrator` console application which applies migrations and also **seeds the initial data**. It is useful on **development** as well as on **production** environment. - -> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. - -Right click to the `.DbMigrator` project and select **Set as StartUp Project** - -![set-as-startup-project](images/set-as-startup-project.png) - - Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: - - ![db-migrator-output](images/db-migrator-output.png) - -> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database (with the password is `1q2w3E*`) which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. - -#### Using EF Core Update-Database Command - -Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations. - -{{ if UI == "MVC" }} - -Right click to the {{if Tiered == "Yes"}}`.IdentityServer`{{else}}`.Web`{{end}} project and select **Set as StartUp project**: - -{{ else if UI != "MVC" }} - -Right click to the `.HttpApi.Host` project and select **Set as StartUp Project**: - -{{ end }} - -![set-as-startup-project](images/set-as-startup-project.png) - -Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations` project as the **Default Project** and run the `Update-Database` command: - -![package-manager-console-update-database](images/package-manager-console-update-database.png) - -This will create a new database based on the configured connection string. - -> **Using the `.DbMigrator` tool is the suggested way**, because it also seeds the initial data to be able to properly run the web application. -> -> If you just use the `Update-Database` command, you will have an empty database, so you can not login to the application since there is no initial admin user in the database. You can use the `Update-Database` command in development time when you don't need to seed the database. However, using the `.DbMigrator` application is easier and you can always use it to migrate the schema and seed the database. - -{{ else if DB == "Mongo" }} - -````json -"ConnectionStrings": { - "Default": "mongodb://localhost:27017/BookStore" -} -```` - -The solution is configured to use **MongoDB** in your local computer, so you need to have a MongoDB server instance up and running or change the connection string to another MongoDB server. - -### Seed Initial Data - -The solution comes with a `.DbMigrator` console application which **seeds the initial data**. It is useful on **development** as well as on **production** environment. - -> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one. - -Right click to the `.DbMigrator` project and select **Set as StartUp Project** - -![set-as-startup-project](images/set-as-startup-project.png) - - Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below: - - ![db-migrator-output](images/db-migrator-output.png) - -> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database (with the password is `1q2w3E*`) which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database. - -{{ end }} - -## Run the Application - -{{ if UI == "MVC" }} - -{{ if Tiered == "Yes" }} - -1. Ensure that the `.IdentityServer` project is the startup project. Run this application that will open a **login** page in your browser. - -> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. - -You can login, but you cannot enter to the main application here. This is **just the authentication server**. - -2. Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a **Swagger UI** in your browser. - -![swagger-ui](images/swagger-ui.png) - -This is the HTTP API that is used by the web application. - -3. Lastly, ensure that the `.Web` project is the startup project and run the application which will open a **welcome** page in your browser - -![mvc-tiered-app-home](images/bookstore-home.png) - -Click to the **login** button which will redirect you to the *authentication server* to login to the application: - -![bookstore-login](images/bookstore-login.png) - -{{ else # Tiered != "Yes" }} - -Ensure that the `.Web` project is the startup project. Run the application which will open the **login** page in your browser: - -> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. - -![bookstore-login](images/bookstore-login.png) - -{{ end # Tiered }} - -{{ else # UI != "MVC" }} - -### Running the HTTP API Host (Server Side) - -{{ if Tiered == "Yes" }} - -Ensure that the `.IdentityServer` project is the startup project. Run the application which will open a **login** page in your browser. - -> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. - -You can login, but you cannot enter to the main application here. This is just the authentication server. - -Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: - -{{ else # Tiered == "No" }} - -Ensure that the `.HttpApi.Host` project is the startup project and run the application which will open a Swagger UI: - -> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. - -{{ end # Tiered }} - -![swagger-ui](images/swagger-ui.png) - -You can see the application APIs and test them here. Get [more info](https://swagger.io/tools/swagger-ui/) about the Swagger UI. - -> ##### Authorization for the Swagger UI -> -> Most of the HTTP APIs require authentication & authorization. If you want to test authorized APIs, manually go to the `/Account/Login` page, enter `admin` as the username and `1q2w3E*` as the password to login to the application. Then you will be able to execute authorized APIs too. - -{{ end # UI }} - -{{ if UI == "Blazor" }} - -### Running the Blazor Application (Client Side) - -Ensure that the `.Blazor` project is the startup project and run the application. - -> Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. - -Once the application starts, click to the **Login** link on to header, which redirects you to the authentication server to enter a username and password: - -![bookstore-login](images/bookstore-login.png) - -{{ else if UI == "NG" }} - -### Running the Angular Application (Client Side) - -Go to the `angular` folder, open a command line terminal, type the `yarn` command (we suggest to the [yarn](https://yarnpkg.com/) package manager while `npm install` will also work) - -```bash -yarn -``` - -Once all node modules are loaded, execute `yarn start` (or `npm start`) command: - -```bash -yarn start -``` - -It may take a longer time for the first build. Once it finishes, it opens the Angular UI in your default browser with the [localhost:4200](http://localhost:4200/) address. - -![bookstore-login](images/bookstore-login.png) - -{{ end }} - -Enter **admin** as the username and **1q2w3E*** as the password to login to the application. The application is up and running. You can start developing your application based on this startup template. - -## Mobile Development - -If you want to include a [React Native](https://reactnative.dev/) project in your solution, add `-m react-native` (or `--mobile react-native`) argument to project creation command. This is a basic React Native startup template to develop mobile applications integrated to your ABP based backends. - -See the [Getting Started with the React Native](Getting-Started-React-Native.md) document to learn how to configure and run the React Native application. - -## Next +This tutorial explains how to **create and run** a new web application using the ABP Framework. Follow the steps below; -* [Web Application Development Tutorial](Tutorials/Part-1.md) +1. [Setup your development environment](Getting-Started-Setup-Environment) +2. [Creating a new solution](Getting-Started-Create-Solution.md) +3. [Running the solution](Getting-Started-Running-Solution.md) \ No newline at end of file diff --git a/docs/en/Index.md b/docs/en/Index.md index 949c04eb24..95052add38 100644 --- a/docs/en/Index.md +++ b/docs/en/Index.md @@ -1,31 +1,73 @@ # ABP Documentation -ABP is an **open source application framework** focused on **ASP.NET Core** based **web application development**. It also supports developing other type of applications. - -Explore the navigation menu to deep dive in the documentation. +ABP Framework is a complete **infrastructure** based on the **ASP.NET Core** to create **modern web applications** and **APIs** by following the software development **best practices** and the **latest technologies**. ## Getting Started -The easiest way to start a new web application with the ABP Framework is to use the [getting started](Getting-Started.md) guide. +* [Getting Started Guide](Getting-Started.md) is the easiest way to start a new web application with the ABP Framework. +* [Web Application Development Tutorial](Tutorials/Part-1.md) is a complete tutorial to develop a full stack web application. -## Tutorials / Articles +### UI Framework Options -### Web Application Development + -[Web application development tutorial](Tutorials/Part-1.md) is a complete tutorial to develop a full stack application using the ABP Framework. +### Database Provider Options -### ABP Community Articles + -See also the [ABP Community](https://community.abp.io/) articles. +## Exploring the Documentation -## Samples +ABP has a **comprehensive documentation** that not only explains the ABP Framework, but also includes **guides** and **samples** to help you on creating a **maintainable solution** by introducing and discussing common **software development principle and best practices**. -See the [sample projects](Samples/Index.md) built with the ABP Framework. +### Architecture + +ABP offers a complete, modular and layered software architecture based on [Domain Driven Design](Domain-Driven-Design.md) principles and patterns. It also provides the necessary infrastructure to implement this architecture. + +* See the [Modularity](Module-Development-Basics.md) document to understand the module system. +* [Implementing Domain Driven Design](Domain-Driven-Design-Implementation-Guide.md) document is an ultimate guide for who want to understand and implement the DDD. +* [Microservice Architecture](Microservice-Architecture.md) document explains how ABP helps to create a microservice solution. + +### Infrastructure + +There are a lot of features provided by the ABP Framework to achieve real world scenarios easier, like [Event Bus](Event-Bus.md), [Background Job System](Background-Jobs.md), [Audit Logging](Audit-Logging.md), [BLOB Storing](Blob-Storing.md), [Data Seeding](Data-Seeding.md), [Data Filtering](Data-Filtering.md). + +### Cross Cutting Concerns + +ABP also simplifies (and even automates wherever possible) cross cutting concerns and common non-functional requirements like [Exception Handling](Exception-Handling.md), [Validation](Validation.md), [Authorization](Authorization.md), [Localization](Localization.md), [Caching](Caching.md), [Dependency Injection](Dependency-Injection.md), [Setting Management](Settings.md), etc. + +### Application Modules + +Application Modules provides pre-built application functionalities; + +* [**Account**](Modules/Account.md): Provides UI for the account management and allows user to login/register to the application. +* **[Identity](Modules/Identity.md)**: Manages organization units, roles, users and their permissions, based on the Microsoft Identity library. +* [**IdentityServer**](Modules/IdentityServer.md): Integrates to IdentityServer4. +* [**Tenant Management**](Modules/Tenant-Management.md): Manages tenants for a [multi-tenant](Multi-Tenancy.md) (SaaS) application. -## Source Code +See the [Application Modules](Modules/Index.md) document for all pre-built modules. + +### Startup Templates + +The [Startup templates](Startup-Templates/Index.md) are pre-built Visual Studio solution templates. You can create your own solution based on these templates to **immediately start your development**. + +## ABP Community + +### The Source Code ABP is hosted on GitHub. See [the source code](https://github.com/abpframework). -## Want to Contribute? +### ABP Community Web Site + +The [ABP Community](https://community.abp.io/) is a website to publish articles and share knowledge about the ABP Framework. You can also create content for the community! + +### Blog + +Follow the [ABP Blog](https://blog.abp.io/) to learn the latest happenings in the ABP Framework. + +### Samples + +See the [sample projects](Samples/Index.md) built with the ABP Framework. + +### Want to Contribute? ABP is a community-driven open source project. See [the contribution guide](Contribution/Index.md) if you want to be a part of this project. diff --git a/docs/en/Integration-Tests.md b/docs/en/Integration-Tests.md index f9f6830956..35d66171a6 100644 --- a/docs/en/Integration-Tests.md +++ b/docs/en/Integration-Tests.md @@ -1,3 +1 @@ -# Integration Tests - -TODO! \ No newline at end of file +This document has been [moved to here](Testing.md). \ No newline at end of file diff --git a/docs/en/Localization.md b/docs/en/Localization.md index a33dff1779..089567422c 100644 --- a/docs/en/Localization.md +++ b/docs/en/Localization.md @@ -162,8 +162,8 @@ Getting the localized text is pretty standard. Just inject the `IStringLocalizer` service and use it like shown below: -````C# -public class MyService +````csharp +public class MyService : ITransientDependency { private readonly IStringLocalizer _localizer; diff --git a/docs/en/Migration-Guides/Abp-4_0-Angular.md b/docs/en/Migration-Guides/Abp-4_0-Angular.md new file mode 100644 index 0000000000..6095a0f447 --- /dev/null +++ b/docs/en/Migration-Guides/Abp-4_0-Angular.md @@ -0,0 +1,121 @@ +# Angular UI 3.3 to 4.0 Migration Guide + +## Angular v11 + +The new ABP Angular UI is based on Angular v11 and TypeScript v4. The difference between v10 and v11 is non-breaking so you do not have to update right away but it is recommended. Nevertheless, ABP modules will keep working with Angular v10. Therefore, if your project is Angular v10, you do not need to update to Angular 11. The update is usually very easy though. + +You can read more about Angular v11 [here](https://blog.angular.io/version-11-of-angular-now-available-74721b7952f7) + +## **Breaking Changes** + +### **Localization** + +Prior to ABP 4.x, we'd handled what locale files of Angular should be created to load them lazily. However, this made it impossible to add new locale files (to be lazily loaded) for our users. With ABP 4.x, we enabled an option to pass a function to `CoreModule`. + +The quickest solution is as follows: + +```typescript +// app.module.ts + +import { registerLocale } from '@abp/ng.core/locale'; +// or +// import { registerLocale } from '@volo/abp.ng.language-management/locale'; +// if you have commercial license + + +@NgModule({ +imports: [ + // ... + CoreModule.forRoot({ + // ...other options, + registerLocaleFn: registerLocale() + }), + //... +] +export class AppModule {} +``` + +You can find the related issue [here](https://github.com/abpframework/abp/issues/6066) +Also, please refer to [the docs](https://docs.abp.io/en/abp/latest/UI/Angular/Localization#registering-a-new-locale) for more information. + +### **Removed the Angular Account Module Public UI** + +With ABP 4.x, we have retired `@abp/ng.account`, it is no longer a part of our framework. There won't be any newer versions of this package as well. Therefore, you can delete anything related to this package. +There should be a config in `app-routing.module` for path `account` and `AccountConfigModule` import in `app.module` + +However, if you are using the commercial version of this package, a.k.a `@volo/abp.ng.account`, this package will continue to exist because it contains `AccountAdminModule` which is still being maintained and developed. You only need to delete the route config from `app-routing.module` + +You can find the related issue [here](https://github.com/abpframework/abp/issues/5652) + +Angular UI is using the Authorization Code Flow to authenticate since version 3.1.0 by default. Starting from version 4.0, this is becoming the only option, because it is the recommended way of authenticating SPAs. + +If you haven't done it yet, see [this post](https://blog.abp.io/abp/ABP-Framework-v3.1-RC-Has-Been-Released) to change the authentication of your application. + +### State Management + +In the ABP Angular UI, we've been using `NGXS` for state management. However, we've decided that the Angular UI should be agnostic with regard to state management. Our users should be able to handle the state in any way they prefer. They should be able to use any library other than `NGXS` or no library at all. That's why we have created our internal store in version 3.2. It is a simple utility class that employs `BehaviorSubject` internally. + +You can examine it [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/utils/internal-store-utils.ts) + +With version 4.0, we will keep utilizing our `InternalStore` instead of `@ngxs/store` in our services and move away from `@ngxs/store`. We plan to remove any dependency of `NGXS` by version 5.0. + +With this in mind, we've already deprecated some services and implemented some breaking changes. + +#### Removed the `SessionState` + +Use `SessionStateService` instead of the `SessionState`. See [this issue](https://github.com/abpframework/abp/issues/5606) for details. + +#### Deprecated the `ConfigState` + +`ConfigState` is now deprecated and should not be used. + +`ConfigState` reference removed from `CoreModule`. If you want to use the `ConfigState` (not recommended), you should pass the state to `NgxsModule` as shown below: + +```typescript +//app.module.ts + +import { ConfigState } from '@abp/ng.core'; + +// ... + +imports: [ + NgxsModule.forRoot([ConfigState]), +// ... +``` + +Moving away from the global store, we create small services with a single responsibility. There are two new services available in version 4.0 which are `EnvironmentService` and `PermissionService`. + +See [the related issue](https://github.com/abpframework/abp/issues/6154) + +Please refer to the following docs for detail information and examples +- [`ConfigStateService`](../UI/Angular/Config-State-Service) +- [`EnvironmentService`](../UI/Angular/Environment#EnvironmentService) +- [`PermissionService`](../UI/Angular/Permission-Management#) + +### Deprecated Interfaces + + Some interfaces have long been marked as deprecated and now they are removed. + +- Removed replaceable components state. +- Removed legacy identity types and service. +- Removed legacy tenant management types and service. +- Removed legacy feature management types and services. +- Removed legacy permission management types and service. + +### Deprecated commercial interfaces +- Removed legacy audit logging types and services. +- Removed legacy identity types and services. +- Removed legacy language management types and services. +- Removed legacy saas types and services. + +### Identity Server [COMMERCIAL] + +With the new version of Identity Server, there happened some breaking changes in the backend (also in the database). We've implemented those in the Angular UI. +If you are just using the package `@volo/abp.ng.identity-server` as is, you will not need to do anything. +However, there are a couple of breaking changes we need to mention. + +- As we have stated above, we want to remove the dependency of `NGXS`. Thus, we have deleted all of the actions defined in `identity-server.actions`. Those actions are not needed anymore and the state is managed locally. With the actions gone, `IdentityServerStateService` became unused and got deleted as well. + +- `ApiScope` is also available as a new entity (It was part of `ApiResource` before). It provides tokens for entity prop, entity actions, toolbar, edit and create form contributors like the existing ones which are `Client`, `IdentityResource` and `ApiResource` + +- There were some deprecated interfaces within the `IdentityServer` namespace. Those are no longer being used, instead, their replacements were generated by `ABP Cli` using the `generate-proxy` command. diff --git a/docs/en/Migration-Guides/Abp-4_0-Blazor.md b/docs/en/Migration-Guides/Abp-4_0-Blazor.md new file mode 100644 index 0000000000..90a9bc2e53 --- /dev/null +++ b/docs/en/Migration-Guides/Abp-4_0-Blazor.md @@ -0,0 +1,88 @@ +# Blazor UI 3.3 to 4.0 Migration Guide + +## Startup Template Changes + +These changes are required to manually applied in your own solution. It would be easier if you create a new solution based on 4.0 with the same name of your current solution then compare the files. + +### Csproj File / Dependencies + +* Add `true` to the `PropertyGroup` section of your project (`.csproj`) file. +* Update the `Blazorise.*` packages to the latest version (to the latest RC for the ABP 4.0 preview). + +### wwwroot/index.html + +There are some changes made in the index.html file; + +* Removed JQuery & Bootstrap JavaScript dependencies +* Replaced Bootstrap and FontAwesome imports with local files instead of CDN usages. +* Re-arranged some ABP CSS file locations. +* Introduced the `abp bundle` CLI command to manage global Style/Script file imports. + +Follow the steps below to apply the changes; + +1. Add the bundle contributor class into your project (it will be slightly different based on your solution namespaces): + +````csharp +using Volo.Abp.Bundling; + +namespace MyCompanyName.MyProjectName.Blazor +{ + public class MyProjectNameBundleContributor : IBundleContributor + { + public void AddScripts(BundleContext context) + { + } + + public void AddStyles(BundleContext context) + { + context.Add("main.css"); + } + } +} +```` + +If you are using another global style/script files, add them here. + +2. Remove all the `` elements and replace with the following comment tags: + +````html + + +```` + +3. Remove all the `` elements and replace with the following comment tags: + +````html + + +```` + +4. Execute the following command in a terminal in the root folder of the Blazor project (`.csproj`) file (ensure that you're using the ABP CLI version 4.0): + +````bash +abp bundle +```` + +This will fill in the `Styles` and `Scripts` tags based on the dependencies. + +5. You can clean the `blazor-error-ui` related sections from your `main.css` file since they are not needed anymore. + +### The Root Element + +This change is optional but recommended. + +* Change `...` to `
...
` in the `wwwroot/index.html`. +* Change `builder.RootComponents.Add("app");` to `builder.RootComponents.Add("#ApplicationContainer");` in the *YourProjectBlazorModule.cs*. + +## AbpCrudPageBase Changes + +If you've derived your pages from the `AbpCrudPageBase` class, then you may need to apply the following changes; + +- `OpenEditModalAsync` method gets `EntityDto` instead of id (`Guid`) parameter. Pass `context` instead of `context.Id`. +- `DeleteEntityAsync` method doesn't display confirmation dialog anymore. You can use the new `EntityActions` component in Data Grids to show confirmation messages. You can also inject `IUiMessageService` to your page or component and call the `ConfirmAsync` explicitly. +- Added `GetListInput` as a base property that is used to filter while getting the entities from the server. + +## Others + +- Refactored namespaces for some Blazor components ([#6015](https://github.com/abpframework/abp/issues/6015)). +- Removed Async Suffix from IUiMessageService methods ([#6123](https://github.com/abpframework/abp/pull/6123)). \ No newline at end of file diff --git a/docs/en/Migration-Guides/Abp-4_0-MVC-Razor-Pages.md b/docs/en/Migration-Guides/Abp-4_0-MVC-Razor-Pages.md new file mode 100644 index 0000000000..d2fa97ec5e --- /dev/null +++ b/docs/en/Migration-Guides/Abp-4_0-MVC-Razor-Pages.md @@ -0,0 +1,6 @@ +# MVC / Razor Pages UI 3.3 to 4.0 Migration Guide + +## Use IBrandingProvider in the Volo.Abp.UI Package + +This will be a breaking change for MVC UI, but very easy to fix. `IBrandingProvider` is being moved from `Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Components` to `Volo.Abp.Ui.Branding` namespace. So, just update the namespace imports. + diff --git a/docs/en/Migration-Guides/Abp-4_0.md b/docs/en/Migration-Guides/Abp-4_0.md index 0d193d476b..9e1bd4e816 100644 --- a/docs/en/Migration-Guides/Abp-4_0.md +++ b/docs/en/Migration-Guides/Abp-4_0.md @@ -1,96 +1,113 @@ # ABP Framework 3.3 to 4.0 Migration Guide -## Auto API Controller Route Changes +This document introduces the breaking changes done in the ABP Framework 4.0 and explains how to fix your 3.x based solutions while upgrading to the ABP Framework 4.0. -The route calculation for the [Auto API Controllers](https://docs.abp.io/en/abp/latest/API/Auto-API-Controllers) is changing with the ABP Framework version 4.0 ([#5325](https://github.com/abpframework/abp/issues/5325)). Previously, **camelCase** route paths were being used. Beginning from the version 4.0, it uses **kebab-case** route paths where it is possible. +> See [the blog post](https://blog.abp.io/abp/ABP.IO-Platform-v4.0-RC-Has-Been-Released-based-on-.NET-5.0) to learn what's new with the ABP Framework 4.0. This document only focuses on the breaking changes. -**A typical auto API before v4.0** +## Overall -![route-before-4](images/route-before-4.png) +Here, the overall list of the changes; -**camelCase route parts become kebab-case with 4.0** +* Upgraded to the .NET 5.0 [(#6118](https://github.com/abpframework/abp/issues/6118)). +* Moved from Newtonsoft.Json to System.Text.Json [(#1198](https://github.com/abpframework/abp/issues/1198)). +* Upgraded to the Identity Server 4.1.1 ([#4461](https://github.com/abpframework/abp/issues/4461)). +* Switched to `kebab-case` for conventional URLs for the auto API controller routes ([#5325](https://github.com/abpframework/abp/issues/5325)). +* Removed Retry for the Dynamic HTTP Client Proxies ([#6090](https://github.com/abpframework/abp/issues/6090)). +* Creation audit properties of the entities made read-only ([#6020](https://github.com/abpframework/abp/issues/6020)). +* Changed type of the IHasExtraProperties.ExtraProperties ([#3751](https://github.com/abpframework/abp/issues/3751)). +* Use IBrandingProvider in the Volo.Abp.UI package and remove the one in the Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared ([#5375](https://github.com/abpframework/abp/issues/5375)). +* Removed the Angular Account Module Public UI (login, register... pages) since they are not being used in the default (authorization code) flow ([#5652](https://github.com/abpframework/abp/issues/5652)). +* Removed the SessionState in the @abp/ng.core package ([#5606](https://github.com/abpframework/abp/issues/5606)). +* Made some API revisions & startup template changes for the Blazor UI. -![route-4](images/route-4.png) +## Upgraded to .NET 5.0 + +ABP Framework has been moved to .NET 5.0. So, if you want to upgrade to the ABP Framework 4.0, you also need to upgrade to .NET 5.0. + +See the [Migrate from ASP.NET Core 3.1 to 5.0](https://docs.microsoft.com/en-us/aspnet/core/migration/31-to-50) document to learn how to upgrade your solution to .NET 5.0. -If it is hard to change it for your application, you can continue to use the version 3.x route strategy, by following one of the approaches; +## Moved to System.Text.Json -* Set `UseV3UrlStyle` to `true` in the options of the `options.ConventionalControllers.Create(...)` method. Example: +ABP Framework 4.0 uses the System.Text.Json by default as the JSON serialization library. It, actually, using a hybrid approach: Continues to use the Newtonsoft.Json when it needs to use features not supported by the System.Text.Json. + +### Unsupported Types + +If you want to use the Newtonsoft.Json to serialize/deserialize for some specific types, you can configure the `AbpSystemTextJsonSerializerOptions` in your module's `ConfigureServices` method. + +**Example: Use Newtonsoft.Json for `MySpecialClass`** ````csharp -options.ConventionalControllers - .Create(typeof(BookStoreApplicationModule).Assembly, opts => - { - opts.UseV3UrlStyle = true; - }); +Configure(options => +{ + options.UnsupportedTypes.AddIfNotContains(typeof(MySpecialClass)); +}); ```` -This approach effects only the controllers for the `BookStoreApplicationModule`. +### Always Use the Newtonsoft.Json -* Set `UseV3UrlStyle` to `true` for the `AbpConventionalControllerOptions` to set it globally. Example: +If you want to continue to use the Newtonsoft.Json library for all the types, you can set `UseHybridSerializer` to false in the `PreConfigureServices` method of your module class: -```csharp -Configure(options => +````csharp +PreConfigure(options => { - options.UseV3UrlStyle = true; + options.UseHybridSerializer = false; }); -``` - -Setting it globally effects all the modules in a modular application. +```` -## Identity Server Changes +## Upgraded to Identity Server 4.1.1 -ABP Framework upgrades the [IdentityServer4](https://www.nuget.org/packages/IdentityServer4) library from 3.x to 4.x with the ABP Framework version 4.0. IdentityServer 4.x has a lot of changes, some of them are **breaking changes in the data structure**. +ABP Framework upgrades the [IdentityServer4](https://www.nuget.org/packages/IdentityServer4) library from 3.x to 4.1.1 with the ABP Framework version 4.0. IdentityServer 4.x has a lot of changes. Some of them are **breaking changes in the data structure**. ### Entity Changes -Entity changed don't directly affect your application, however it is good to know. +Entity changes don't directly affect your application; however, it is good to know. #### ApiScope -As the **most important breaking change**, Identity Server 4.x places the `ApiScope` as an independent aggregate root. Previously it was a part of the to `ApiResource` aggregate. This requires manual operation. See the *Database Changes* section. +As the **most critical breaking change**; Identity Server 4.x defines the `ApiScope` as an independent aggregate root. Previously, it was the child entity of the `ApiResource`. This change requires manual operation. See the _Database Changes_ section. Also, added `Enabled(string)` and `Description(bool,true)` properties. #### ApiResource -* Added `AllowedAccessTokenSigningAlgorithms (string)` and `ShowInDiscoveryDocument(bool, default: true)` properties +- Added `AllowedAccessTokenSigningAlgorithms (string)` and `ShowInDiscoveryDocument(bool, default: true)` properties #### Client -* Added `RequireRequestObject (bool)` and `AllowedIdentityTokenSigningAlgorithms (string)` properties. -* Changed default value of `RequireConsent` from `true` to `false`. -* Changed default value of `RequirePkce` from `false` to `true`. +- Added `RequireRequestObject ` and `AllowedIdentityTokenSigningAlgorithms ` properties. +- Changed the default value of `RequireConsent` from `true` to `false`. +- Changed the default value of `RequirePkce` from `false` to `true`. #### DeviceFlowCodes -* Added `SessionId (string)` and `Description (string)` properties. +- Added `SessionId ` and `Description ` properties. #### PersistedGrant -* Added `SessionId (string)` and `Description(string)` and `ConsumedTime (DateTime?)` properties +- Added `SessionId `, `Description ` and `ConsumedTime ` properties ### Database Changes > Attention: **Please backup your database** before the migration! -**If you are upgrading from 3.x, then there are some change should be done in your database.** +**If you are upgrading from 3.x, then there are some steps should be done in your database.** #### Database Schema Migration -If you are using **Entity Framework Core**, you need to add a new database migration, using the `Add-Migration` command, and apply changes to the database. Please **review the migration** script and read the sections below to understand if it affects your existing data. Otherwise, you may **loose some of your configuration**, which may not be easy to remember and re-configure. +If you are using **Entity Framework Core**, you need to add a new database migration, using the `Add-Migration` command, and apply changes to the database. Please **review the migration** script and read the sections below to understand if it affects your existing data. Otherwise, you may **lose some of your configuration**, which may not be easy to remember and re-configure. #### Seed Code -If you haven't customize the `IdentityServerDataSeedContributor` and haven't customized the initial data inside the `IdentityServer*` tables; +If you haven't customized the `IdentityServerDataSeedContributor` and haven't customized the initial data inside the `IdentityServer*` tables; 1. Update `IdentityServerDataSeedContributor` class by comparing to [the latest code](https://github.com/abpframework/abp/blob/dev/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain/IdentityServer/IdentityServerDataSeedContributor.cs). You probably only need to add the `CreateApiScopesAsync` method and the code related to it. -2. Then you can simply clear all the **table data** in these tables then execute the `DbMigrator` application again to fill it with the new configuration. +2. Then you can simply clear all the **data** in these tables then execute the `DbMigrator` application to fill it with the new configuration. #### Migrating the Configuration Data -If you've customize your IdentityServer configuration in the database or in the seed data, you should understand the changes and upgrade your code/data accordingly. Especially, the following changes will affect your application: +If you've customized your IdentityServer configuration in the database or in the seed data, you should understand the changes and upgrade your code/data accordingly. Especially, the following changes will affect your application: -- `IdentityServerApiScopes` table's `Enabled` field is dropped and re-created. So, you need to manually enable the api scopes again. +- `IdentityServerApiScopes` table's `Enabled` field is dropped and re-created. So, you need to enable the API scopes again manually. - `IdentityServerApiResourceScopes` table is dropped and recreated. So, you need to backup and move your current data to the new table. - `IdentityServerIdentityResourceClaims` table is dropped and recreated. So, you need to backup and move your current data to the new table. @@ -98,11 +115,11 @@ You may need to perform additional steps based on how much you made custom confi ### Other IdentityServer Changes -IdentityServer has removed the [public origin option](https://github.com/IdentityServer/IdentityServer4/pull/4335). It was resolving http/https conversion problems, but they decided to leave this to the developer. This is especially needed if you use a reverse proxy where your external protocol is HTTPS but internal protocol is HTTP. +IdentityServer has removed the [public origin option](https://github.com/IdentityServer/IdentityServer4/pull/4335). It was resolving HTTP/HTTPS conversion issues, but they decided to leave this to the developer. This is especially needed if you use a reverse proxy where your external protocol is HTTPS but internal protocol is HTTP. -One simple solution is to add such a middleware into your ASP.NET Core pipeline, at the beginning. +One simple solution is to add such a middleware at the begingning of your ASP.NET Core pipeline. -``` +```csharp app.Use((httpContext, next) => { httpContext.Request.Scheme = "https"; @@ -113,5 +130,151 @@ app.Use((httpContext, next) => > This sample is obtained from the [ASP.NET Core documentation](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer#scenarios-and-use-cases). You can use it if you always use HTTPS in all environments. ### Related Resources -* https://leastprivilege.com/2020/06/19/announcing-identityserver4-v4-0/ -* https://github.com/IdentityServer/IdentityServer4/issues/4592 + +- https://leastprivilege.com/2020/06/19/announcing-identityserver4-v4-0/ +- https://github.com/IdentityServer/IdentityServer4/issues/4592 + +## Auto API Controller Route Changes + +The route calculation for the [Auto API Controllers](https://docs.abp.io/en/abp/latest/API/Auto-API-Controllers) is changing with the ABP Framework version 4.0 ([#5325](https://github.com/abpframework/abp/issues/5325)). Before v4.0 the route paths were **camelCase**. After version 4.0, it's changed to **kebab-case** route paths where it is possible. + +**A typical auto API before v4.0** + +![route-before-4](images/route-before-4.png) + +**camelCase route parts become kebab-case with 4.0** + +![route-4](images/route-4.png) + +### How to Fix? + +You may not take any action for the MVC & Blazor UI projects. + +For the Angular UI, this change may effect your client UI. If you have used the [ABP CLI Service Proxy Generation](../UI/Angular/Service-Proxies.md), you can run the server side and re-generate the service proxies. If you haven't used this tool, you should manually update the related URLs in your application. + +If there are other type of clients (e.g. 3rd-party companies) using your APIs, they also need to update the URLs. + +### Use the v3.x style URLs + +If it is hard to change it in your application, you can still to use the version 3.x route strategy, by following one of the approaches; + +- Set `UseV3UrlStyle` to `true` in the options of the `options.ConventionalControllers.Create(...)` method. Example: + +```csharp +options.ConventionalControllers + .Create(typeof(BookStoreApplicationModule).Assembly, opts => + { + opts.UseV3UrlStyle = true; + }); +``` + +This approach affects only the controllers for the `BookStoreApplicationModule`. + +- Set `UseV3UrlStyle` to `true` for the `AbpConventionalControllerOptions` to set it globally. Example: + +```csharp +Configure(options => +{ + options.UseV3UrlStyle = true; +}); +``` + +Setting it globally affects all the modules in a modular application. + +## Removed Retry for the Dynamic HTTP Client Proxies + +[Dynamic C# HTTP Client Proxies](../API/Dynamic-CSharp-API-Clients.md) were trying up to 3 times if a request fails using the [Polly](https://github.com/App-vNext/Polly) library. Starting from the version 4.0, this logic has been removed. If you need it, you should configure it in your own application, by configuring the `AbpHttpClientBuilderOptions` in the `PreConfigureServices` method of your module. + +**Example: Retry 3 times on failure by incremental waiting between tries** + +````csharp +public override void PreConfigureServices(ServiceConfigurationContext context) +{ + PreConfigure(options => + { + options.ProxyClientBuildActions.Add((remoteServiceName, clientBuilder) => + { + clientBuilder.AddTransientHttpErrorPolicy( + policyBuilder => policyBuilder + .WaitAndRetryAsync(3, i => TimeSpan.FromSeconds(Math.Pow(2, i))) + ); + }); + }); +} +```` + +This example uses the Microsoft.Extensions.Http.Polly NuGet package. + +If you create a new solution, you can find the same configuration in the `.HttpApi.Client.ConsoleTestApp` project's module class, as an example. + +## Creation Audit Properties Made Read-Only + +Removed setters from the `IHasCreationTime.CreationTime`, ` IMustHaveCreator.CreatorId` and `IMayHaveCreator.CreatorId` properties to accidently set the creation properties while updating an existing entity. + +Since the ABP Framework automatically sets these properties, you normally don't need to directly set them. If you want to set them, as a best practice, it is suggested to make it in the constructor to not provide a way to change it later. + +These properties implemented with `protected set` in the `Entity` and `AggregateRoot` base classes. That means you can still set in a derived class, if you need it. Alternatively, you can use reflection to set them (Or use `ObjectHelper.TrySetProperty` which internally uses reflection) out of the class if you have to do. + +## Changed type of the IHasExtraProperties.ExtraProperties + +`IHasExtraProperties.ExtraProperties` was a regular `Dictionary`. With the version 4.0, it is replaced with `ExtraPropertyDictionary` class which inherits the `Dictionary`. + +Most of the applications don't be affected by this change. If you've directly implemented this interface, replace the standard dictionary to the `ExtraPropertyDictionary`. + +## Other Changes + +### IdentityOptions Usage + +Previously, when you inject `IOptions`, you get a dynamically overridden options value. For example, when you get `IdentityOptions.Password.RequiredLength`, the value is being changed based on the setting (`IdentitySettingNames.Password.RequiredLength`) of the current tenant. That means `IdentityOptions` changes per tenant. However, this caused an [issue](https://github.com/abpframework/abp/issues/6318) and we [had to change](https://github.com/abpframework/abp/pull/6333) the usage. + +With the version 4.0, you need to inject `IOptions` and call the new `SetAsync` method before using it, to be able to override the options by the settings. Otherwise, you get the default (statically configured) values of the options. + +Example usage: + +````csharp +public class MyService : ITransientDependency +{ + private readonly IOptions _options; + + public MyService(IOptions options) + { + _options = options; + } + + public async Task DoItAsync() + { + await _options.SetAsync(); + + var requiredLength = _options.Value.Password.RequiredLength; + } +} +```` + +Pre-built modules already handles this. However, if you have used `IdentityOptions` directly in your code, you also need to follow this new pattern. +Please make sure that the injected `IOptions` service and the service consuming it are in the same scope of dependency injection container. + +### LDAP module full async + +In order to solve the problem of async over sync, `ILdapManager` uses async method instead of sync. And use [`ldap4net`](https://github.com/flamencist/ldap4net) to replace [`Novell.Directory.Ldap.NETStandard`](https://github.com/dsbenghe/Novell.Directory.Ldap.NETStandard) package. + +### Dynamic external login provider system + +You need to change the `WithDynamicOptions` method and pass the `Handler` class of the external login provider. +Use the `goto definition` function in Visual Studio or Rider to check `Handler` in the extension method like `AddGoogle`. + +```csharp +- WithDynamicOptions() ++ WithDynamicOptions() +```` + +## ASP.NET Core MVC / Razor Pages UI + +See the [ASP.NET Core MVC / Razor Pages UI Migration Guide](Abp-4_0-MVC-Razor-Pages.md). + +## Angular UI + +See the [Angular UI Migration Guide](Abp-4_0-Angular.md). + +## Blazor UI + +See the [Blazor UI Migration Guide](Abp-4_0-Blazor.md). diff --git a/docs/en/Migration-Guides/Index.md b/docs/en/Migration-Guides/Index.md new file mode 100644 index 0000000000..5973ee0a0d --- /dev/null +++ b/docs/en/Migration-Guides/Index.md @@ -0,0 +1,5 @@ +# ABP Framework Migration Guides + +* [3.3.x to 4.0 Migration Guide](Abp-4_0.md) +* [2.9.x to 3.0 Migration Guide](../UI/Angular/Migration-Guide-v3.md) + diff --git a/docs/en/Module-Entity-Extensions.md b/docs/en/Module-Entity-Extensions.md index 61a7093dc5..dc23d1e663 100644 --- a/docs/en/Module-Entity-Extensions.md +++ b/docs/en/Module-Entity-Extensions.md @@ -1,3 +1,497 @@ # Module Entity Extensions -See https://docs.abp.io/en/commercial/latest/guides/module-entity-extensions (it will be moved here soon). \ No newline at end of file +> This feature is not supported by the Blazor UI yet. + +## Introduction + +Module entity extension system is a **high level** extension system that allows you to **define new properties** for existing entities of the depended modules. It automatically **adds properties to the entity, database, HTTP API and the user interface** in a single point. + +> The module must be developed the *Module Entity Extensions* system in mind. All the **official modules** supports this system wherever possible. + +## Quick Example + +Open the *YourProjectNameModuleExtensionConfigurator* class inside the `Domain.Shared` project of your solution and change the `ConfigureExtraProperties`method as shown below to add a `SocialSecurityNumber` property to the `IdentityUser` entity of the [Identity Module](Modules/Identity.md). + +````csharp +public static void ConfigureExtraProperties() +{ + OneTimeRunner.Run(() => + { + ObjectExtensionManager.Instance.Modules() + .ConfigureIdentity(identity => + { + identity.ConfigureUser(user => + { + user.AddOrUpdateProperty( //property type: string + "SocialSecurityNumber", //property name + property => + { + //validation rules + property.Attributes.Add(new RequiredAttribute()); + property.Attributes.Add( + new StringLengthAttribute(64) { + MinimumLength = 4 + } + ); + + //...other configurations for this property + } + ); + }); + }); + }); +} +```` + +>This method is called inside the `YourProjectNameDomainSharedModule` at the beginning of the application. `OneTimeRunner` is a utility class that guarantees to execute this code only one time per application, since multiple calls are unnecessary. + +* `ObjectExtensionManager.Instance.Modules()` is the starting point to configure a module. `ConfigureIdentity(...)` method is used to configure the entities of the Identity Module. +* `identity.ConfigureUser(...)` is used to configure the user entity of the identity module. Not all entities are designed to be extensible (since it is not needed). Use the intellisense to discover the extensible modules and entities. +* `user.AddOrUpdateProperty(...)` is used to add a new property to the user entity with the `string` type (`AddOrUpdateProperty` method can be called multiple times for the same property of the same entity. Each call can configure the options of the same property, but only one property is added to the entity with the same property name). You can call this method with different property names to add more properties. +* `SocialSecurityNumber` is the name of the new property. +* `AddOrUpdateProperty` gets a second argument (the `property =>` lambda expression) to configure additional options for the new property. + * We can add data annotation attributes like shown here, just like adding a data annotation attribute to a class property. + +#### Create & Update Forms + +Once you define a property, it appears in the create and update forms of the related entity: + +![add-new-property-to-user-form](images/add-new-property-to-user-form.png) + +`SocialSecurityNumber` field comes into the form. Next sections will explain the localization and the validation for this new property. + +### Data Table + +New properties also appear in the data table of the related page: + +![add-new-property-to-user-form](images/add-new-property-to-user-table.png) + +`SocialSecurityNumber` column comes into the table. Next sections will explain the option to hide this column from the data table. + +## Property Options + +There are some options that you can configure while defining a new property. + +### Display Name + +You probably want to set a different (human readable) display name for the property that is shown on the user interface. + +#### Don't Want to Localize? + +If your application is not localized, you can directly set the `DisplayName` for the property to a `FixedLocalizableString` object. Example: + +````csharp +property => +{ + property.DisplayName = new FixedLocalizableString("Social security no"); +} +```` + +#### Localizing the Display Name + +If you want to localize the display name, you have two options. + +##### Localize by Convention + +Instead of setting the `property.DisplayName`, you can directly open your localization file (like `en.json`) and add the following entry to the `texts` section: + +````json +"SocialSecurityNumber": "Social security no" +```` + +Define the same `SocialSecurityNumber` key (the property name you've defined before) in your localization file for each language you support. That's all! + +In some cases, the localization key may conflict with other keys in your localization files. In such cases, you can use the `DisplayName:` prefix for display names in the localization file (`DisplayName:SocialSecurityNumber` as the localization key for this example). Extension system looks for prefixed version first, then fallbacks to the non prefixed name (it then fallbacks to the property name if you haven't localized it). + +> This approach is recommended since it is simple and suitable for most scenarios. + +##### Localize using the `DisplayName` Property + +If you want to specify the localization key or the localization resource, you can still set the `DisplayName` option: + +````csharp +property => +{ + property.DisplayName = + LocalizableString.Create( + "UserSocialSecurityNumberDisplayName" + ); +} +```` + +* `MyProjectNameResource` is the localization resource and `UserSocialSecurityNumberDisplayName` is the localization key in the localization resource. + +> See [the localization document](Localization.md) if you want to learn more about the localization system. + +#### Default Value + +A default value is automatically set for the new property, which is the natural default value for the property type, like `null` for `string`, `false` for `bool` or `0` for `int`. + +There are two ways to override the default value: + +##### DefaultValue Option + +`DefaultValue` option can be set to any value: + +````csharp +property => +{ + property.DefaultValue = 42; +} +```` + +##### DefaultValueFactory Options + +`DefaultValueFactory` can be set to a function that returns the default value: + +````csharp +property => +{ + property.DefaultValueFactory = () => DateTime.Now; +} +```` + +`options.DefaultValueFactory` has a higher priority than the `options.DefaultValue` . + +> Tip: Use `DefaultValueFactory` option only if the default value may change over the time (like `DateTime.Now` in this example). If it is a constant value, then use the `DefaultValue` option. + +### Validation + +Entity extension system allows you to define validation for extension properties in a few ways. + +#### Data Annotation Attributes + +`Attributes` is a list of attributes associated to this property. The example code below adds two [data annotation validation attributes](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation) to the property: + +````csharp +property => +{ + property.Attributes.Add(new RequiredAttribute()); + property.Attributes.Add(new StringLengthAttribute(64) {MinimumLength = 4}); +} +```` + +When you run the application, you see that the validation works out of the box: + +![add-new-propert-to-user-form](images/add-new-property-to-user-form-validation-error.png) + +Since we've added the `RequiredAttribute`, it doesn't allow to left it blank. The validation system works; + +* On the user interface (with automatic localization). +* On the HTTP API. Even if you directly perform an HTTP request, you get validation errors with a proper HTTP status code. +* On the `SetProperty(...)` method on the entity (see [the document](Entities.md) if you wonder what is the `SetProperty()` method). + +So, it automatically makes a full stack validation. + +> See the [ASP.NET Core MVC Validation document](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation) to learn more about the attribute based validation. + +##### Default Validation Attributes + +There are some attributes **automatically added** when you create certain type of properties; + +* `RequiredAttribute` is added for **non nullable** primitive property types (e.g. `int`, `bool`, `DateTime`...) and `enum` types. If you want to allow nulls, make the property nullable (e.g. `int?`). +* `EnumDataTypeAttribute` is added for **enum types**, to prevent to set invalid enum values. + +Use `property.Attributes.Clear();` if you don't want these attributes. + +#### Validation Actions + +Validation actions allows you to execute a custom code to perform the validation. The example below checks if the `SocialSecurityNumber` starts with `B` and adds a validation error if so: + +````csharp +property => +{ + property.Attributes.Add(new RequiredAttribute()); + property.Attributes.Add(new StringLengthAttribute(64) {MinimumLength = 4}); + + property.Validators.Add(context => + { + if (((string) context.Value).StartsWith("B")) + { + context.ValidationErrors.Add( + new ValidationResult( + "Social security number can not start with the letter 'B', sorry!", + new[] {"extraProperties.SocialSecurityNumber"} + ) + ); + } + }); + +} +```` + +Using a `RegularExpressionAttribute` might be better in this case, but this is just an example. Anyway, if you enter a value starts with the letter `B` you get the following error **while saving the form**: + +![add-new-propert-to-user-form](images/add-new-property-to-user-form-validation-error-custom.png) + +##### The Context Object + +The `context` object has useful properties that can be used in your custom validation action. For example, you can use the `context.ServiceProvider` to resolve services from the [dependency injection system](Dependency-Injection.md). The example below gets the localizer and adds a localized error message: + +````csharp +if (((string) context.Value).StartsWith("B")) +{ + var localizer = context.ServiceProvider + .GetRequiredService>(); + + context.ValidationErrors.Add( + new ValidationResult( + localizer["SocialSecurityNumberCanNotStartWithB"], + new[] {"extraProperties.SocialSecurityNumber"} + ) + ); +} +```` + +>`context.ServiceProvider` is nullable! It can be `null` only if you use the `SetProperty(...)` method on the object. Because DI system is not available on this time. While this is a rare case, you should perform a fallback logic when `context.ServiceProvider` is `null`. For this example, you would add a non-localized error message. This is not a problem since setting an invalid value to a property generally is a programmer mistake and you mostly don't need to localization in this case. In any way, you would not be able to use localization even in a regular property setter. But, if you are serious about localization, you can throw a business exception (see the [exception handling document](https://docs.abp.io/en/abp/latest/Exception-Handling) to learn how to localize a business exception). + +### UI Visibility + +When you define a property, it appears on the data table, create and edit forms on the related UI page. However, you can control each one individually. Example: + +````csharp +property => +{ + property.UI.OnTable.IsVisible = false; + //...other configurations +} +```` + +Use `property.UI.OnCreateForm` and `property.UI.OnEditForm` to control forms too. If a property is required, but not added to the create form, you definitely get a validation exception, so use this option carefully. But a required property may not be in the edit form if that's your requirement. + +### HTTP API Availability + +Even if you disable a property on UI, it can be still available through the HTTP API. By default, a property is available on all APIs. + +Use the `property.Api` options to make a property unavailable in some API endpoints. + +````csharp +property => +{ + property.Api.OnUpdate.IsAvailable = false; +} +```` + +In this example, Update HTTP API will not allow to set a new value to this property. In this case, you also want to disable this property on the edit form: + +````csharp +property => +{ + property.Api.OnUpdate.IsAvailable = false; + property.UI.OnEditForm.IsVisible = false; +} +```` + +In addition to the `property.Api.OnUpdate`, you can set `property.Api.OnCreate` and `property.Api.OnGet` for a fine control the API endpoint. + +## Special Types + +### Enum + +Module extension system naturally supports the `enum` types. + +An example enum type: + +````csharp +public enum UserType +{ + Regular, + Moderator, + SuperUser +} +```` + +You can add enum properties just like others: + +````csharp +user.AddOrUpdateProperty("Type"); +```` + +An enum properties is shown as combobox (select) in the create/edit forms: + +![add-new-property-enum](images/add-new-property-enum.png) + +#### Localization + +Enum member name is shown on the table and forms by default. If you want to localize it, just create a new entry on your [localization](https://docs.abp.io/en/abp/latest/Localization) file: + +````json +"UserType.SuperUser": "Super user" +```` + +One of the following names can be used as the localization key: + +* `Enum:UserType.SuperUser` +* `UserType.SuperUser` +* `SuperUser` + +Localization system searches for the key with the given order. Localized text are used on the table and the create/edit forms. + +### Navigation Properties / Foreign Keys + +It is supported to add an extension property to an entity that is Id of another entity (foreign key). + +#### Example: Associate a department to a user + +````csharp +ObjectExtensionManager.Instance.Modules() + .ConfigureIdentity(identity => + { + identity.ConfigureUser(user => + { + user.AddOrUpdateProperty( + "DepartmentId", + property => + { + property.UI.Lookup.Url = "/api/departments"; + property.UI.Lookup.DisplayPropertyName = "name"; + } + ); + }); + }); +```` + +`UI.Lookup.Url` option takes a URL to get list of departments to select on edit/create forms. This endpoint can be a typical controller, an [auto API controller](API/Auto-API-Controllers.md) or any type of endpoint that returns a proper JSON response. + +An example implementation that returns a fixed list of departments (in real life, you get the list from a data source): + +````csharp +[Route("api/departments")] +public class DepartmentController : AbpController +{ + [HttpGet] + public async Task> GetAsync() + { + return new ListResultDto( + new[] + { + new DepartmentDto + { + Id = Guid.Parse("6267f0df-870f-4173-be44-d74b4b56d2bd"), + Name = "Human Resources" + }, + new DepartmentDto + { + Id = Guid.Parse("21c7b61f-330c-489e-8b8c-80e0a78a5cc5"), + Name = "Production" + } + } + ); + } +} +```` + +This API returns such a JSON response: + +````json +{ + "items": [{ + "id": "6267f0df-870f-4173-be44-d74b4b56d2bd", + "name": "Human Resources" + }, { + "id": "21c7b61f-330c-489e-8b8c-80e0a78a5cc5", + "name": "Production" + }] +} +```` + +ABP can now show an auto-complete select component to pick the department while creating or editing a user: + +![extension-navigation-property-form](images/extension-navigation-property-form.png) + +And shows the department name on the data table: + +![extension-navigation-property-form](images/extension-navigation-property-table.png) + +#### Lookup Options + +`UI.Lookup` has the following options to customize how to read the response returned from the `Url`: + +* `Url`: The endpoint to get the list of target entities. This is used on edit and create forms. +* `DisplayPropertyName`: The property in the JSON response to read the display name of the target entity to show on the UI. Default: `text`. +* `ValuePropertyName`: The property in the JSON response to read the Id of the target entity. Default: `id`. +* `FilterParamName`: ABP allows to search/filter the entity list on edit/create forms. This is especially useful if the target list contains a lot of items. In this case, you can return a limited list (top 100, for example) and allow user to search on the list. ABP sends filter text to the server (as a simple query string) with the name of this option. Default: `filter`. +* `ResultListPropertyName`: By default, returned JSON result should contain the entity list in an `items` array. You can change the name of this field. Default: `items`. + +#### Lookup Properties: How Display Name Works? + +You may wonder how ABP shows the department name on the data table above. + +It is easy to understand how to fill the dropdown on edit and create forms: ABP makes an AJAX request to the given URL. It re-requests whenever user types to filter the items. + +However, for the data table, multiple items are shown on the UI and performing a separate AJAX call to get display name of the department for each row would not be so efficient. + +Instead, the display name of the foreign entity is also saved as an extra property of the entity (see *Extra Properties* section of the [Entities](Entities.md) document) in addition to Id of the foreign entity. If you check the database, you can see the `DepartmentId_Text` in the `ExtraProperties` field in the database table: + +````json +{"DepartmentId":"21c7b61f-330c-489e-8b8c-80e0a78a5cc5","DepartmentId_Text":"Production"} +```` + +So, this is a type of *data duplication*. If your target entity's name changes in the database later, there is no automatic synchronization system. The system works as expected, but you see the old name on the data tables. If that's a problem for you, you should care yourself to update this information when display name of your entity changes. + +## Database Mapping + +For relational databases, all extension property values are stored in a single field in the table: + +![add-new-propert-to-user-database-extra-properties](images/add-new-propert-to-user-database-extra-properties.png) + +`ExtraProperties` field stores the properties as a JSON object. While that's fine for some scenarios, you may want to create a dedicated field for your new property. Fortunately, it is very easy to configure. + +If you are using the Entity Framework Core database provider, you can configure the database mapping as shown below: + +````csharp +ObjectExtensionManager.Instance + .MapEfCoreProperty( + "SocialSecurityNumber", + (entityBuilder, propertyBuilder) => + { + propertyBuilder.HasMaxLength(64); + } + ); +```` + +Write this inside the `YourProjectNameEfCoreEntityExtensionMappings` class in your `.EntityFrameworkCore` project. Then you need to use the standard `Add-Migration` and `Update-Database` commands to create a new database migration and apply the change to your database. + +Add-Migration create a new migration as shown below: + +````csharp +public partial class Added_SocialSecurityNumber_To_IdentityUser : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SocialSecurityNumber", + table: "AbpUsers", + maxLength: 128, + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SocialSecurityNumber", + table: "AbpUsers"); + } +} +```` + +Once you update your database, you will see that the `AbpUsers` table has the new property as a standard table field: + +![add-new-propert-to-user-database-extra-properties](images/add-new-propert-to-user-database-field.png) + +> If you first created a property without a database table field, then you later needed to move this property to a database table field, it is suggested to execute an SQL command in your migration to copy the old values to the new field. +> +> However, if you don't make it, the ABP Framework seamlessly manages it. It uses the new database field, but fallbacks to the `ExtraProperties` field if it is null. When you save the entity, it moves the value to the new field. + +See the [Extending Entities](Customizing-Application-Modules-Extending-Entities.md) document for more. + +## More + +See the [Customizing the Modules](Customizing-Application-Modules-Guide.md) guide for an overall index for all the extensibility options. + +Here, a few things you can do: + +* You can create a second entity that maps to the same database table with the extra property as a standard class property (if you've defined the EF Core mapping). For the example above, you can add a `public string SocialSecurityNumber {get; set;}` property to the `AppUser` entity in your application, since the `AppUser` entity is mapped to the same `AbpUser` table. Do this only if you need it, since it brings more complexity to your application. +* You can override a domain or application service to perform custom logics with your new property. +* You can low level control how to add/render a field in the data table on the UI. + diff --git a/docs/en/Modules/Account.md b/docs/en/Modules/Account.md index 6901b38f59..94cdb7af95 100644 --- a/docs/en/Modules/Account.md +++ b/docs/en/Modules/Account.md @@ -1,3 +1,38 @@ # Account Module -TODO \ No newline at end of file +This module provides necessary UI pages/components to make the user login and register to the application. + +> This document is incomplete. + +## Social/External Logins + +The [Account Module](../Modules/Account.md) has already configured to handle social or external logins out of the box. You can follow the ASP.NET Core documentation to add a social/external login provider to your application. + +### Example: Facebook Authentication + +Follow the [ASP.NET Core Facebook integration document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to support the Facebook login for your application. + +#### Add the NuGet Package + +Add the [Microsoft.AspNetCore.Authentication.Facebook](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Facebook) package to your project. Based on your architecture, this can be `.Web`, `.IdentityServer` (for tiered setup) or `.Host` project. + +#### Configure the Provider + +Use the `.AddFacebook(...)` extension method in the `ConfigureServices` method of your [module](../Module-Development-Basics.md), to configure the client: + +````csharp +context.Services.AddAuthentication() + .AddFacebook(facebook => + { + facebook.AppId = "..."; + facebook.AppSecret = "..."; + facebook.Scope.Add("email"); + facebook.Scope.Add("public_profile"); + }); +```` + +> It would be a better practice to use the `appsettings.json` or the ASP.NET Core User Secrets system to store your credentials, instead of a hard-coded value like that. Follow the [Microsoft's document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to learn the user secrets usage. + +### Other UI Types + +Beginning from the v3.1, the [Angular UI](../UI/Angular/Quick-Start.md) uses authorization code flow (as a best practice) to authenticate the user by redirecting to the MVC UI login page. So, even if you are using the Angular UI, social/external login integration is same as explained above and it will work out of the box. As similar, The [Blazor UI](../UI/Blazor/Overall.md) also uses the MVC UI to logic. \ No newline at end of file diff --git a/docs/en/Modules/Client-Simulation.md b/docs/en/Modules/Client-Simulation.md new file mode 100644 index 0000000000..bc5d38e814 --- /dev/null +++ b/docs/en/Modules/Client-Simulation.md @@ -0,0 +1,3 @@ +# Client Simulation Module + +TODO \ No newline at end of file diff --git a/docs/en/Modules/Cms-Kit.md b/docs/en/Modules/Cms-Kit.md new file mode 100644 index 0000000000..e13f1e585b --- /dev/null +++ b/docs/en/Modules/Cms-Kit.md @@ -0,0 +1,3 @@ +# CMS Kit Module + +TODO \ No newline at end of file diff --git a/docs/en/Modules/Docs.md b/docs/en/Modules/Docs.md index e788a4c0f8..35478d9ce0 100644 --- a/docs/en/Modules/Docs.md +++ b/docs/en/Modules/Docs.md @@ -47,7 +47,7 @@ The database connection string is located in `appsettings.json` of your `Acme.My ```json { "ConnectionStrings": { - "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True" } } ``` @@ -446,7 +446,7 @@ As an example you can see ABP Framework documentation: #### Conditional sections feature (Using Scriban) -Docs module uses [Scriban]( ) for conditionally show or hide some parts of a document. In order to use that feature, you have to create a JSON file as **Parameter document** per every language. It will contain all the key-values, as well as their display names. +Docs module uses [Scriban](https://github.com/lunet-io/scriban/tree/master/doc) for conditionally show or hide some parts of a document. In order to use that feature, you have to create a JSON file as **Parameter document** per every language. It will contain all the key-values, as well as their display names. For example, [en/docs-params.json](https://github.com/abpio/abp-commercial-docs/blob/master/en/docs-params.json): @@ -537,7 +537,7 @@ Also, **Document_Language_Code** and **Document_Version** keys are pre-defined i ------ -**IMPORTANT NOTICE**: Scriban uses "{{" and "}}" for syntax. Therefore, you must use escape blocks if you are going to use those in your document (an Angular document, for example). See [Scriban docs]( ) for more information. +**IMPORTANT NOTICE**: Scriban uses "{{" and "}}" for syntax. Therefore, you must use escape blocks if you are going to use those in your document (an Angular document, for example). See [Scriban docs](https://github.com/lunet-io/scriban/blob/master/doc/language.md#13-escape-block) for more information. ### 8- Creating the Navigation Document diff --git a/docs/en/Modules/Identity.md b/docs/en/Modules/Identity.md index b38f59c03b..8cd7bf3966 100644 --- a/docs/en/Modules/Identity.md +++ b/docs/en/Modules/Identity.md @@ -1,8 +1,8 @@ # Identity Management Module -Identity module is used to manage [organization units](Organization-Units.md), roles, users and their permissions, based on the Microsoft Identity library. +Identity module is used to manage organization units, roles, users and their permissions, based on the Microsoft Identity library. -**See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon...** +> **See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon...** ## Identity Security Log @@ -27,3 +27,51 @@ Configure(options => options.ApplicationName = "AbpSecurityTest"; }); ``` + +## Organization Unit Management + +Organization units (OU) is a part of **Identity Module** and can be used to **hierarchically group users and entities**. + +### OrganizationUnit Entity + +An OU is represented by the **OrganizationUnit** entity. The fundamental properties of this entity are: + +- **TenantId**: Tenant's Id of this OU. Can be null for host OUs. +- **ParentId**: Parent OU's Id. Can be null if this is a root OU. +- **Code**: A hierarchical string code that is unique for a tenant. +- **DisplayName**: Shown name of the OU. + +The OrganizationUnit entity's primary key (Id) is a **Guid** type and it derives from the [**FullAuditedAggregateRoot**](../Entities.md) class. + +#### Organization Tree + +Since an OU can have a parent, all OUs of a tenant are in a **tree** structure. There are some rules for this tree; + +- There can be more than one root (where the `ParentId` is `null`). +- There is a limit for the first-level children count of an OU (because of the fixed OU Code unit length explained below). + +#### OU Code + +OU code is automatically generated and maintained by the OrganizationUnit Manager. It's a string that looks something like this: + +"**00001.00042.00005**" + +This code can be used to easily query the database for all the children of an OU (recursively). There are some rules for this code: + +- It must be **unique** for a [tenant](../Multi-Tenancy.md). +- All the children of the same OU have codes that **start with the parent OU's code**. +- It's **fixed length** and based on the level of the OU in the tree, as shown in the sample. +- While the OU code is unique, it can be **changeable** if you move an OU. +- You must reference an OU by Id, not Code. + +### OrganizationUnit Manager + +The **OrganizationUnitManager** class can be [injected](../Dependency-Injection.md) and used to manage OUs. Common use cases are: + +- Create, Update or Delete an OU +- Move an OU in the OU tree. +- Getting information about the OU tree and its items. + +#### Multi-Tenancy + +The `OrganizationUnitManager` is designed to work for a **single tenant** at a time. It works for the **current tenant** by default. \ No newline at end of file diff --git a/docs/en/Modules/Index.md b/docs/en/Modules/Index.md index ae17c15c8a..8851f8b554 100644 --- a/docs/en/Modules/Index.md +++ b/docs/en/Modules/Index.md @@ -1,32 +1,34 @@ # Application Modules -ABP is a **modular application framework** which consists of dozens of **nuget packages**. It also provides a complete infrastructure to build your own application modules which may have entities, services, database integration, APIs, UI components and so on. +ABP is a **modular application framework** which consists of dozens of **NuGet & NPM packages**. It also provides a complete infrastructure to build your own application modules which may have entities, services, database integration, APIs, UI components and so on. -There are **two types of modules.** They don't have any structural difference but categorized by functionality and purpose: +There are **two types of modules.** They don't have any structural difference but are categorized by functionality and purpose: * [**Framework modules**](https://github.com/abpframework/abp/tree/master/framework/src): These are **core modules of the framework** like caching, emailing, theming, security, serialization, validation, EF Core integration, MongoDB integration... etc. They do not have application/business functionalities but makes your daily development easier by providing common infrastructure, integration and abstractions. * [**Application modules**](https://github.com/abpframework/abp/tree/master/modules): These modules implement specific application/business functionalities like blogging, document management, identity management, tenant management... etc. They generally have their own entities, services, APIs and UI components. ## Open Source Application Modules -There are some **free and open source** application modules developed and maintained by the ABP community: +There are some **free and open source** application modules developed and maintained as a part of the ABP Framework. -* **Account**: Provides UI for the account management and allows user to login/register to the application. +* [**Account**](Account.md): Provides UI for the account management and allows user to login/register to the application. * [**Audit Logging**](Audit-Logging.md): Persists audit logs to a database. -* **Background Jobs**: Persist background jobs when using the default background job manager. -* **Blogging**: Used to create fancy blogs. ABP's [own blog](https://blog.abp.io/) already using this module. -* [**Docs**](Docs.md): Used to create technical documentation pages. ABP's [own documentation](https://docs.abp.io) already using this module. -* **Feature Management**: Used to persist and manage the [features](../Features.md). +* [**Background Jobs**](Background-Jobs.md): Persist background jobs when using the default background job manager. +* [**Blogging**](Blogging.md): Used to create fancy blogs. ABP's [own blog](https://blog.abp.io/) already using this module. +* [**Client Simulation**](Client-Simulation.md): A simple web UI to stress test HTTP APIs by simulating concurrent clients. +* [**CMS Kit**](Cms-Kit.md): A set of reusable *Content Management System* features. +* [**Docs**](Docs.md): Used to create technical documentation website. ABP's [own documentation](https://docs.abp.io) already using this module. +* [**Feature Management**](Feature-Management.md): Used to persist and manage the [features](../Features.md). * **[Identity](Identity.md)**: Manages organization units, roles, users and their permissions, based on the Microsoft Identity library. -* **IdentityServer**: Integrates to IdentityServer4. -* **Permission Management**: Used to persist permissions. +* [**IdentityServer**](IdentityServer.md): Integrates to IdentityServer4. +* [**Permission Management**](Permission-Management.md): Used to persist permissions. * **[Setting Management](Setting-Management.md)**: Used to persist and manage the [settings](../Settings.md). -* **Tenant Management**: Manages tenants for a [multi-tenant](../Multi-Tenancy.md) application. -* **Users**: Abstract users, so other modules can depend on this module instead of the Identity module. +* [**Tenant Management**](Tenant-Management.md): Manages tenants for a [multi-tenant](../Multi-Tenancy.md) application. +* [**Users**](Users.md): Abstract users, so other modules can depend on this module instead of the Identity module. * [**Virtual File Explorer**](Virtual-File-Explorer.md): Provided a simple UI to view files in [virtual file system](../Virtual-File-System.md). See [the GitHub repository](https://github.com/abpframework/abp/tree/master/modules) for source code of all modules. ## Commercial Application Modules -[ABP Commercial](https://commercial.abp.io/) license provides additional pre-built application modules on top of the ABP framework. See the [module list](https://commercial.abp.io/modules) provided by the ABP Commercial. \ No newline at end of file +[ABP Commercial](https://commercial.abp.io/) license provides **additional pre-built application modules** on top of the ABP framework. See the [module list](https://commercial.abp.io/modules) provided by the ABP Commercial. \ No newline at end of file diff --git a/docs/en/Modules/Organization-Units.md b/docs/en/Modules/Organization-Units.md deleted file mode 100644 index b27e038528..0000000000 --- a/docs/en/Modules/Organization-Units.md +++ /dev/null @@ -1,47 +0,0 @@ -# Organization Unit Management - -Organization units (OU) is a part of **Identity Module** and can be used to **hierarchically group users and entities**. - -### OrganizationUnit Entity - -An OU is represented by the **OrganizationUnit** entity. The fundamental properties of this entity are: - -- **TenantId**: Tenant's Id of this OU. Can be null for host OUs. -- **ParentId**: Parent OU's Id. Can be null if this is a root OU. -- **Code**: A hierarchical string code that is unique for a tenant. -- **DisplayName**: Shown name of the OU. - -The OrganizationUnit entity's primary key (Id) is a **Guid** type and it derives from the [**FullAuditedAggregateRoot**](../Entities.md) class. - -#### Organization Tree - -Since an OU can have a parent, all OUs of a tenant are in a **tree** structure. There are some rules for this tree; - -- There can be more than one root (where the `ParentId` is `null`). -- There is a limit for the first-level children count of an OU (because of the fixed OU Code unit length explained below). - -#### OU Code - -OU code is automatically generated and maintained by the OrganizationUnit Manager. It's a string that looks something like this: - -"**00001.00042.00005**" - -This code can be used to easily query the database for all the children of an OU (recursively). There are some rules for this code: - -- It must be **unique** for a [tenant](../Multi-Tenancy.md). -- All the children of the same OU have codes that **start with the parent OU's code**. -- It's **fixed length** and based on the level of the OU in the tree, as shown in the sample. -- While the OU code is unique, it can be **changeable** if you move an OU. -- You must reference an OU by Id, not Code. - -### OrganizationUnit Manager - -The **OrganizationUnitManager** class can be [injected](../Dependency-Injection.md) and used to manage OUs. Common use cases are: - -- Create, Update or Delete an OU -- Move an OU in the OU tree. -- Getting information about the OU tree and its items. - -#### Multi-Tenancy - -The `OrganizationUnitManager` is designed to work for a **single tenant** at a time. It works for the **current tenant** by default. diff --git a/docs/en/Modules/Setting-Management.md b/docs/en/Modules/Setting-Management.md index 134e24aed2..dff8da3f8e 100644 --- a/docs/en/Modules/Setting-Management.md +++ b/docs/en/Modules/Setting-Management.md @@ -75,7 +75,7 @@ Setting values are cached using the [distributed cache](../Caching.md) system. A ## Setting Management Providers -Setting Management module is extensible, just like the [setting system](../Settings.md). You can extend it by defining setting management providers. There are 5 pre-built setting management providers registered by the order below: +Setting Management module is extensible, just like the [setting system](../Settings.md). You can extend it by defining setting management providers. There are 5 pre-built setting management providers registered it the following order: * `DefaultValueSettingManagementProvider`: Gets the value from the default value of the setting definition. It can not set the default value since default values are hard-coded on the setting definition. * `ConfigurationSettingManagementProvider`: Gets the value from the [IConfiguration service](../Configuration.md). It can not set the configuration value because it is not possible to change the configuration values on runtime. diff --git a/docs/en/Modules/Users.md b/docs/en/Modules/Users.md new file mode 100644 index 0000000000..1cb0f4b701 --- /dev/null +++ b/docs/en/Modules/Users.md @@ -0,0 +1,3 @@ +# Users Module + +TODO \ No newline at end of file diff --git a/docs/en/MongoDB.md b/docs/en/MongoDB.md index 475eb94f1b..7fae9c3892 100644 --- a/docs/en/MongoDB.md +++ b/docs/en/MongoDB.md @@ -40,7 +40,7 @@ public class MyDbContext : AbpMongoDbContext protected override void CreateModel(IMongoModelBuilder modelBuilder) { base.CreateModel(modelBuilder); - + //Customize the configuration for your collections. } } @@ -62,7 +62,7 @@ So, most of times you don't need to explicitly configure registration for your e protected override void CreateModel(IMongoModelBuilder modelBuilder) { base.CreateModel(modelBuilder); - + modelBuilder.Entity(b => { b.CollectionName = "MyQuestions"; //Sets the collection name @@ -88,7 +88,7 @@ If you have multiple databases in your application, you can configure the connec [ConnectionStringName("MySecondConnString")] public class MyDbContext : AbpMongoDbContext { - + } ```` @@ -202,7 +202,7 @@ You generally want to derive from the `IRepository` to inherit standard reposito Example implementation of the `IBookRepository` interface: ```csharp -public class BookRepository : +public class BookRepository : MongoDbRepository, IBookRepository { @@ -242,9 +242,9 @@ context.Services.AddMongoDbContext(options => This is especially important when you want to **override a base repository method** to customize it. For instance, you may want to override `DeleteAsync` method to delete an entity in a more efficient way: ```csharp -public override async Task DeleteAsync( - Guid id, - bool autoSave = false, +public async override Task DeleteAsync( + Guid id, + bool autoSave = false, CancellationToken cancellationToken = default) { //TODO: Custom implementation of the delete method @@ -381,4 +381,45 @@ context.Services.AddMongoDbContext(options => }); ``` -In this example, `OtherMongoDbContext` implements `IBookStoreMongoDbContext`. This feature allows you to have multiple MongoDbContext (one per module) on development, but single MongoDbContext (implements all interfaces of all MongoDbContexts) on runtime. \ No newline at end of file +In this example, `OtherMongoDbContext` implements `IBookStoreMongoDbContext`. This feature allows you to have multiple MongoDbContext (one per module) on development, but single MongoDbContext (implements all interfaces of all MongoDbContexts) on runtime. + +### Customize Bulk Operations + +If you have better logic or using an external library for bulk operations, you can override the logic via implementing `IMongoDbBulkOperationProvider`. + +- You may use example template below: + +```csharp +public class MyCustomMongoDbBulkOperationProvider : IMongoDbBulkOperationProvider, ITransientDependency +{ + public async Task DeleteManyAsync(IMongoDbRepository repository, + IEnumerable entities, + IClientSessionHandle sessionHandle, + bool autoSave, + CancellationToken cancellationToken) + where TEntity : class, IEntity + { + // Your logic here. + } + + public async Task InsertManyAsync(IMongoDbRepository repository, + IEnumerable entities, + IClientSessionHandle sessionHandle, + bool autoSave, + CancellationToken cancellationToken) + where TEntity : class, IEntity + { + // Your logic here. + } + + public async Task UpdateManyAsync(IMongoDbRepository repository, + IEnumerable entities, + IClientSessionHandle sessionHandle, + bool autoSave, + CancellationToken cancellationToken) + where TEntity : class, IEntity + { + // Your logic here. + } +} +``` \ No newline at end of file diff --git a/docs/en/Object-Extensions.md b/docs/en/Object-Extensions.md index 308be4bfa7..72265b2690 100644 --- a/docs/en/Object-Extensions.md +++ b/docs/en/Object-Extensions.md @@ -410,3 +410,7 @@ ObjectExtensionManager.Instance ```` See the [Entity Framework Core Integration document](Entity-Framework-Core.md) for more. + +## See Also + +* [Module Entity Extensions](Module-Entity-Extensions.md) \ No newline at end of file diff --git a/docs/en/PlugIn-Modules.md b/docs/en/PlugIn-Modules.md new file mode 100644 index 0000000000..1d613582ab --- /dev/null +++ b/docs/en/PlugIn-Modules.md @@ -0,0 +1,233 @@ +# Plug-In Modules + +It is possible to load [modules](Module-Development-Basics.md) as plug-ins. That means you may not reference to a module's assembly in your solution, but you can load that module in the application startup just like any other module. + +## Basic Usage + +`IServiceCollection.AddApplication()` extension method can get options to configure the plug-in sources. + +**Example: Load plugins from a folder** + +````csharp +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity.PlugIns; + +namespace MyPlugInDemo.Web +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddApplication(options => + { + options.PlugInSources.AddFolder(@"D:\Temp\MyPlugIns"); + }); + } + + public void Configure(IApplicationBuilder app) + { + app.InitializeApplication(); + } + } +} +```` + +* This is the `Startup` class of a typical ASP.NET Core application. +* `PlugInSources.AddFolder` gets a folder path and to load assemblies (typically `dll`s) in that folder. + +That's all. ABP will discover the modules in the given folder, configure and initialize them just like regular modules. + +### Plug-In Sources + +`options.PlugInSources` is actually a list of `IPlugInSource` implementations and `AddFolder` is just a shortcut for the following expression: + +````csharp +options.PlugInSources.Add(new FolderPlugInSource(@"D:\Temp\MyPlugIns")); +```` + +> `AddFolder()` only looks for the assembly file in the given folder, but not looks for the sub-folders. You can pass `SearchOption.AllDirectories` as a second parameter to explore plug-ins also from the sub-folders, recursively. + +There are two more built-in Plug-In Source implementations: + +* `PlugInSources.AddFiles()` gets a list of assembly (typically `dll`) files. This is a shortcut of using `FilePlugInSource` class. +* `PlugInSources.AddTypes()` gets a list of module class types. If you use this, you need to load the assemblies of the modules yourself, but it provides flexibility when needed. This is a shortcut of using `TypePlugInSource` class. + +If you need, you can create your own `IPlugInSource` implementation and add to the `options.PlugInSources` just like the others. + +## Example: Creating a Simple Plug-In + +Create a simple **Class Library Project** in a solution: + +![simple-plugin-library](images/simple-plugin-library.png) + +You can add ABP Framework packages you need to use in the module. At least, you should add the `Volo.Abp.Core` package to the project: + +```` +Install-Package Volo.Abp.Core +```` + +Every [module](Module-Development-Basics.md) must declare a class derived from the `AbpModule`. Here, a simple module class that resolves a service and initializes it on the application startup: + +````csharp +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp; +using Volo.Abp.Modularity; + +namespace MyPlugIn +{ + public class MyPlungInModule : AbpModule + { + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var myService = context.ServiceProvider + .GetRequiredService(); + + myService.Initialize(); + } + } +} +```` + +`MyService` can be any class registered to [Dependency Injection](Dependency-Injection.md) system, as show below: + +````csharp +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; + +namespace MyPlugIn +{ + public class MyService : ITransientDependency + { + private readonly ILogger _logger; + + public MyService(ILogger logger) + { + _logger = logger; + } + + public void Initialize() + { + _logger.LogInformation("MyService has been initialized"); + } + } +} +```` + +Build the project, open the build folder, find the `MyPlugIn.dll`: + +![simple-plug-in-dll-file](images/simple-plug-in-dll-file.png) + +Copy `MyPlugIn.dll` into the plug-in folder (`D:\Temp\MyPlugIns` for this example). + +If you have configured the main application like described above (see Basic Usage section), you should see the `MyService has been initialized` log in the application startup. + +## Example: Creating a Plug-In With Razor Pages + +Creating plug-ins with views inside requires a bit more attention. + +> This example assumes you've [created a new web application](https://abp.io/get-started) using the application startup template and MVC / Razor Pages UI. + +Create a new **Class Library** project in a solution: + +![simple-razor-plugin](images/simple-razor-plugin.png) + +Edit the `.csproj` file content: + +````xml + + + + net5.0 + Library + true + + + + + + + +```` + +* Changed `Sdk` to `Microsoft.NET.Sdk.Web`. +* Added `OutputType` and `IsPackable` properties. +* Added `Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared` NuGet package. + +> Depending on [Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared) package is not required. You can reference to a more base package like [Volo.Abp.AspNetCore.Mvc](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc/). However, if you will build a UI page/component, it is suggested to reference to the [Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared) package since it is the most high-level package without depending on a particular [theme](UI/AspNetCore/Theming.md). If there is no problem to depend on a particular theme, you can directly reference to the theme's package to be able to use the theme-specific features in your plug-in. + +Then create your module class in the plug-in: + +````csharp +using System.IO; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; +using Volo.Abp.Modularity; + +namespace MyMvcUIPlugIn +{ + [DependsOn(typeof(AbpAspNetCoreMvcUiThemeSharedModule))] + public class MyMvcUIPlugInModule : AbpModule + { + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(mvcBuilder => + { + //Add plugin assembly + mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(typeof(MyMvcUIPlugInModule).Assembly)); + + //Add views assembly + var viewDllPath = Path.Combine(Path.GetDirectoryName(typeof(MyMvcUIPlugInModule).Assembly.Location), "MyMvcUIPlugIn.Views.dll"); + var viewAssembly = new CompiledRazorAssemblyPart(Assembly.LoadFrom(viewDllPath)); + mvcBuilder.PartManager.ApplicationParts.Add(viewAssembly); + }); + } + } +} +```` + +* Depending on the `AbpAspNetCoreMvcUiThemeSharedModule` since we added the related NuGet package. +* Adding the plug-in's assembly to the `PartManager` of ASP.NET Core MVC. This is required by ASP.NET Core. Otherwise, your controllers inside the plug-in doesn't work. +* Adding the plug-in's views assembly to the `PartManager` of ASP.NET Core MVC. This is required by ASP.NET Core. Otherwise, your views inside the plug-in doesn't work. + +You can now add a razor page, like `MyPlugInPage.cshtml` inside the `Pages` folder: + +````html +@page +@model MyMvcUIPlugIn.Pages.MyPlugInPage +

Welcome to my plug-in page

+

This page is located inside a plug-in module! :)

+```` + +Now, you can build the plug-in project. It will produce the following output: + +![simple-razor-plug-in-dll-file](images/simple-razor-plug-in-dll-file.png) + +Copy the `MyMvcUIPlugIn.dll` and `MyMvcUIPlugIn.Views.dll` into the plug-in folder (`D:\Temp\MyPlugIns` for this example). + +If you have configured the main application like described above (see Basic Usage section), you should be able to visit the `/MyPlugInPage` URL when your application: + +![simple-plugin-output](images/simple-plugin-output.png) + +## Discussions + +In real world, your plug-in may have some external dependencies. Also, your application might be designed to support plug-ins. All these are your own system requirements. What ABP does is just loading modules on the application startup. What you do inside that modules is up to you. + +However, we can provide a few suggestions for some common cases. + +### Library Dependencies + +For package/dll dependencies, you can copy the related dlls to the plug-in folder. ABP automatically loads all assemblies in the folder and your plug-in will work as expected. + +> See [Microsoft's documentation](https://docs.microsoft.com/en-us/dotnet/core/tutorials/creating-app-with-plugin-support#plugin-with-library-dependencies) for some additional explanations for that case. + +### Database Schema + +If your module uses a relational database and [Entity Framework Core](Entity-Framework-Core.md), it will need to have its tables available in the database. There are different ways to ensure the tables have been created when an application uses the plug-in. Some examples; + +1. The Plugin may check if the database tables does exists and create the tables on the application startup or migrate them if the plug-in has been updated and requires some schema changes. You can use EF Core's migration API to do that. +2. You can improve the `DbMigrator` application to find migrations of the plug-ins and execute them. + +There may be other solutions. For example, if your DB admin doesn't allow you to change the database schema in the application code, you may need to manually send a SQL file to the database admin to apply it to the database. \ No newline at end of file diff --git a/docs/en/Repositories.md b/docs/en/Repositories.md index 0fc80984e7..a2eb385094 100644 --- a/docs/en/Repositories.md +++ b/docs/en/Repositories.md @@ -13,61 +13,168 @@ ABP can provide a **default generic repository** for each aggregate root or enti **Example usage of a default generic repository:** ````C# -public class PersonAppService : ApplicationService -{ - private readonly IRepository _personRepository; +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; - public PersonAppService(IRepository personRepository) +namespace Demo +{ + public class PersonAppService : ApplicationService { - _personRepository = personRepository; - } + private readonly IRepository _personRepository; - public async Task Create(CreatePersonDto input) - { - var person = new Person { Name = input.Name, Age = input.Age }; + public PersonAppService(IRepository personRepository) + { + _personRepository = personRepository; + } - await _personRepository.InsertAsync(person); - } + public async Task CreateAsync(CreatePersonDto input) + { + var person = new Person(input.Name); - public List GetList(string nameFilter) - { - var people = _personRepository - .Where(p => p.Name.Contains(nameFilter)) - .ToList(); + await _personRepository.InsertAsync(person); + } - return people - .Select(p => new PersonDto {Id = p.Id, Name = p.Name, Age = p.Age}) - .ToList(); + public async Task GetCountAsync(string filter) + { + return await _personRepository.CountAsync(p => p.Name.Contains(filter)); + } } } ```` -> See the "*IQueryable & Async Operations*" section below to understand how you can use **async extension methods**, like `ToListAsync()` (which is strongly suggested) instead of `ToList()`. - In this example; * `PersonAppService` simply injects `IRepository` in it's constructor. -* `Create` method uses `InsertAsync` to save a newly created entity. -* `GetList` method uses the standard LINQ `Where` and `ToList` methods to filter and get a list of people from the data source. +* `CreateAsync` method uses `InsertAsync` to save the new entity. +* `GetCountAsync` method gets a filtered count of all people in the database. -> The example above uses hand-made mapping between [entities](Entities.md) and [DTO](Data-Transfer-Objects.md)s. See [object to object mapping document](Object-To-Object-Mapping.md) for an automatic way of mapping. +### Standard Repository Methods Generic Repositories provides some standard CRUD features out of the box: -* Provides `Insert` method to save a new entity. +* `GetAsync`: Returns a single entity by its `Id` or a predicate (lambda expression). + * Throws `EntityNotFoundException` if the requested entity was not found. + * Throws `InvalidOperationException` if there are multiple entities with given predicate. +* `FindAsync`: Returns a single entity by its `Id` or a predicate (lambda expression). + * Returns `null` if the requested entity was not found. + * Throws `InvalidOperationException` if there are multiple entities with given predicate. +* `InsertAsync`: Inserts a new entity to the database. +* `UpdateAsync`: Updates an existing entity in the database. +* `DeleteAsync`: Deletes the given entity from database. + * This method has an overload that takes a predicate (lambda expression) to delete multiple entities satisfies the given condition. +* `GetListAsync`: Returns the list of all entities in the database. +* `GetPagedListAsync`: Returns a limited list of entities. Gets `skipCount`, `maxResultCount` and `sorting` parameters. +* `GetCountAsync`: Gets count of all entities in the database. + +There are overloads of these methods. + * Provides `Update` and `Delete` methods to update or delete an entity by entity object or it's id. * Provides `Delete` method to delete multiple entities by a filter. -* Implements `IQueryable`, so you can use LINQ and extension methods like `FirstOrDefault`, `Where`, `OrderBy`, `ToList` and so on... -### Basic Repositories +### Querying / LINQ over the Repositories + +Repositories provide the `GetQueryableAsync()` method that returns an `IQueryable` object. You can use this object to perform LINQ queries on the entities in the database. + +**Example: Use LINQ with the repositories** + +````csharp +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace Demo +{ + public class PersonAppService : ApplicationService + { + private readonly IRepository _personRepository; + + public PersonAppService(IRepository personRepository) + { + _personRepository = personRepository; + } + + public async Task> GetListAsync(string filter) + { + //Obtain the IQueryable + IQueryable queryable = await _personRepository.GetQueryableAsync(); + + //Create a query + var query = from person in queryable + where person.Name == filter + orderby person.Name + select person; -Standard `IRepository` interface extends standard `IQueryable` and you can freely query using standard LINQ methods. However, some ORM providers or database systems may not support standard `IQueryable` interface. + //Execute the query to get list of people + var people = query.ToList(); + + //Convert to DTO and return to the client + return people.Select(p => new PersonDto {Name = p.Name}).ToList(); + } + } +} +```` + +You could also use the LINQ extension methods: + +````csharp +public async Task> GetListAsync(string filter) +{ + //Obtain the IQueryable + IQueryable queryable = await _personRepository.GetQueryableAsync(); + + //Execute a query + var people = queryable + .Where(p => p.Name.Contains(filter)) + .OrderBy(p => p.Name) + .ToList(); + + //Convert to DTO and return to the client + return people.Select(p => new PersonDto {Name = p.Name}).ToList(); +} +```` + +Any standard LINQ method can be used over the `IQueryable` returned from the repository. + +> This sample uses `ToList()` method, but it is **strongly suggested to use the asynchronous methods** to perform database queries, like `ToListAsync()` for this example. +> +> See the **IQueryable & Async Operations** section to learn how you can do it. + +### Bulk Operations + +There are some methods to perform bulk operations in the database; + +* `InsertManyAsync` +* `UpdateManyAsync` +* `DeleteManyAsync` + +These methods work with multiple entities and can take advantage of bulk operations if supported by the underlying database provider. + +> Optimistic concurrency control may not be possible when you use `UpdateManyAsync` and `DeleteManyAsync` methods. + +### Soft / Hard Delete + +`DeleteAsync` method of the repository doesn't delete the entity if the entity is a **soft-delete** entity (that implements `ISoftDelete`). Soft-delete entities are marked as "deleted" in the database. Data Filter system ensures that the soft deleted entities are not retrieved from database normally. + +If your entity is a soft-delete entity, you can use the `HardDeleteAsync` method to physically delete the entity from database in case of you need it. + +> See the [Data Filtering](Data-Filtering.md) documentation for more about soft-delete. + +## Other Generic Repository Types + +Standard `IRepository` interface exposes the standard `IQueryable` and you can freely query using the standard LINQ methods. This is fine for most of the applications. However, some ORM providers or database systems may not support standard `IQueryable` interface. If you want to use such providers, you can't rely on the `IQueryable`. + +### Basic Repositories ABP provides `IBasicRepository` and `IBasicRepository` interfaces to support such scenarios. You can extend these interfaces (and optionally derive from `BasicRepositoryBase`) to create custom repositories for your entities. -Depending on `IBasicRepository` but not depending on `IRepository` has an advantage to make possible to work with all data sources even if they don't support `IQueryable`. But major vendors, like Entity Framework, NHibernate or MongoDb already support `IQueryable`. +Depending on `IBasicRepository` but not depending on `IRepository` has an advantage to make possible to work with all data sources even if they don't support `IQueryable`. -So, working with `IRepository` is the **suggested** way for typical applications. But reusable module developers may consider `IBasicRepository` to support a wider range of data sources. +Major vendors, like Entity Framework, NHibernate or MongoDB already support `IQueryable`. So, working with `IRepository` is the **suggested** way for typical applications. But reusable module developers may consider `IBasicRepository` to support a wider range of data sources. ### Read Only Repositories diff --git a/docs/en/Road-Map.md b/docs/en/Road-Map.md index 8670ba080b..6f4213a6bd 100644 --- a/docs/en/Road-Map.md +++ b/docs/en/Road-Map.md @@ -1,17 +1,13 @@ # ABP Framework Road Map -You can always check the milestone planning and the prioritized backlog issues on [the GitHub repository](https://github.com/abpframework/abp/milestones) for a detailed road map. - -While we will **continue to add other exciting features**, we will work on the following major items in the **middle term**: - -* **Blazor UI** for the framework and all the pre-built modules (in progress). -* **.NET 5.0**! As Microsoft has announced that the .NET 5.0 will be released in November 2020, we will prepare for this change before and move to the .NET 5.0 just after Microsoft releases it. We hope a smooth transition. - -Beside this middle term goals, there are many features in the [backlog](https://github.com/abpframework/abp/milestone/2). Here, a list of some major items in the backlog; +You can always check the milestone planning and the prioritized backlog issues on [the GitHub repository](https://github.com/abpframework/abp/milestones) for a detailed road map. Here, a list of some major items in the backlog; +* CMS Kit: A set of reusable, extensible and composable Content Management System features. * [#2882](https://github.com/abpframework/abp/issues/2882) / Providing a gRPC integration infrastructure (while it is [already possible](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo) to create or consume gRPC endpoints for your application, we plan to create endpoints for the [standard application modules](https://docs.abp.io/en/abp/latest/Modules/Index)) * [#236](https://github.com/abpframework/abp/issues/236) Resource based authorization system +* [#6132](https://github.com/abpframework/abp/issues/6132) A New Theme alternative to the Basic Theme * [#1754](https://github.com/abpframework/abp/issues/1754) / Multi-lingual entities +* [#497](https://github.com/abpframework/abp/issues/497) API Versioning system finalize & document * [#633](https://github.com/abpframework/abp/issues/633) / Realtime notification system * [#57](https://github.com/abpframework/abp/issues/57) / Built-in CQRS infrastructure * [#336](https://github.com/abpframework/abp/issues/336) / Health Check abstraction diff --git a/docs/en/Samples/Index.md b/docs/en/Samples/Index.md index f298ea49f5..18312e4b55 100644 --- a/docs/en/Samples/Index.md +++ b/docs/en/Samples/Index.md @@ -17,6 +17,9 @@ A simple CRUD application to show basic principles of developing an application * **Book Store: Razor Pages UI & Entity Framework Core** * [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC&DB=EF) * [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* **Book Store: Blazor UI & Entity Framework Core** + * [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=Blazor&DB=EF) + * [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * **Book Store: Angular UI & MongoDB** * [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG&DB=Mongo) * [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) @@ -27,6 +30,9 @@ While there is no Razor Pages & MongoDB combination, you can check both document ### Other Samples +* **Event Organizer**: A sample application to create events (meetups) and allow others to register the events. Developed using EF Core and Blazor UI. + * [Source code](https://github.com/abpframework/abp-samples/tree/master/EventOrganizer) + * [Article](https://community.abp.io/articles/creating-an-event-organizer-application-with-the-blazor-ui-wbe0sf2z) * **Entity Framework Migrations**: A solution to demonstrate how to split your application into multiple databases each database contains different modules. * [Source code](https://github.com/abpframework/abp-samples/tree/master/EfCoreMigrationDemo) * [EF Core database migrations document](../Entity-Framework-Core-Migrations.md) @@ -59,6 +65,17 @@ While there is no Razor Pages & MongoDB combination, you can check both document * [Customize the SignIn Manager](https://community.abp.io/articles/how-to-customize-the-signin-manager-3e858753) * **GRPC Demo**: Shows how to add a gRPC service to an ABP Framework based web application and consume it from a console application. * [Source code](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo) +* **Telerik Blazor Integration**: Shows how to install and use Telerik Blazor components with the ABP Framework. + * [Article](https://community.abp.io/articles/how-to-integrate-the-telerik-blazor-components-to-the-abp-blazor-ui-q8g31abb) +* **Angular Material Integration**: Implemented the web application tutorial using the Angular Material library. + * [Source code](https://github.com/abpframework/abp-samples/tree/master/AcmeBookStoreAngularMaterial) + * [Article](https://community.abp.io/articles/using-angular-material-components-with-the-abp-framework-af8ft6t9) +* **DevExtreme Angular Component Integration**: How to install and use DevExtreme components in the ABP Framework Angular UI. + * [Source code](https://github.com/abpframework/abp-samples/tree/master/DevExtreme-Angular) + * [Article](https://community.abp.io/articles/using-devextreme-angular-components-with-the-abp-framework-x5nyvj3i) +* **DevExtreme MVC / Razor Pages Component Integration**: How to install and use DevExtreme components in the ABP Framework MVC / Razor Pages UI. + * [Source code](https://github.com/abpframework/abp-samples/tree/master/DevExtreme-Mvc) + * [Article](https://community.abp.io/articles/using-devextreme-components-with-the-abp-framework-zb8z7yqv) * **Empty ASP.NET Core Application**: The most basic ASP.NET Core application with the ABP Framework installed. * [Source code](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication) * [Documentation](../Getting-Started-AspNetCore-Application.md) \ No newline at end of file diff --git a/docs/en/Samples/Microservice-Demo.md b/docs/en/Samples/Microservice-Demo.md index 9b4c622c4e..285c2b10a0 100644 --- a/docs/en/Samples/Microservice-Demo.md +++ b/docs/en/Samples/Microservice-Demo.md @@ -19,7 +19,7 @@ This sample aims to demonstrate a simple yet complete microservice solution; * Has a **console application** to show the simplest way of using a service by authenticating. * Uses [Redis](https://redis.io/) for **distributed caching**. * Uses [RabbitMQ](https://www.rabbitmq.com/) for service-to-service **messaging**. -* Uses [Docker](https://www.docker.com/) & [Kubernates](https://kubernetes.io/) to **deploy** & run all services and applications. +* Uses [Docker](https://www.docker.com/) & [Kubernetes](https://kubernetes.io/) to **deploy** & run all services and applications. * Uses [Elasticsearch](https://www.elastic.co/products/elasticsearch) & [Kibana](https://www.elastic.co/products/kibana) to store and visualize the logs (written using [Serilog](https://serilog.net/)). The diagram below shows the system: @@ -842,7 +842,7 @@ It has a dedicated MongoDB database (MsDemo_Blogging) to store blog and posts. I ````json "ConnectionStrings": { - "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true", + "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True", "Blogging": "mongodb://localhost/MsDemo_Blogging" } ```` @@ -968,8 +968,8 @@ There are two connection strings in the `appsettings.json` file: ````json "ConnectionStrings": { - "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true", - "ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True", + "ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True" } ```` @@ -1416,4 +1416,4 @@ An Audit Log record has a `CorrelationId` property that can be used to track a r ### Multi-Tenancy -The solution has been configured to provide a [multi-tenant](../Multi-Tenancy.md) system, where each tenant can have their isolated users, roles, permissions and other data. \ No newline at end of file +The solution has been configured to provide a [multi-tenant](../Multi-Tenancy.md) system, where each tenant can have their isolated users, roles, permissions and other data. diff --git a/docs/en/Specifications.md b/docs/en/Specifications.md index ea4f140e95..036baa749e 100644 --- a/docs/en/Specifications.md +++ b/docs/en/Specifications.md @@ -1,3 +1,255 @@ # Specifications -TODO! \ No newline at end of file +Specification Pattern is used to define **named, reusable, combinable and testable filters** for entities and other business objects. + +> A Specification is a part of the Domain Layer. + +## Installation + +> This package is **already installed** when you use the startup templates. So, most of the times you don't need to manually install it. + +Install the [Volo.Abp.Specifications](https://abp.io/package-detail/Volo.Abp.Specifications) package to your project. You can use the [ABP CLI](CLI.md) *add-package* command in a command line terminal when the current folder is the root folder of your project (`.csproj`): + +````bash +abp add-package Volo.Abp.Specifications +```` + +## Defining the Specifications + +Assume that you've a Customer entity as defined below: + +````csharp +using System; +using Volo.Abp.Domain.Entities; + +namespace MyProject +{ + public class Customer : AggregateRoot + { + public string Name { get; set; } + + public byte Age { get; set; } + + public long Balance { get; set; } + + public string Location { get; set; } + } +} +```` + +You can create a new Specification class derived from the `Specification`. + +**Example: A specification to select the customers with 18+ age:** + +````csharp +using System; +using System.Linq.Expressions; +using Volo.Abp.Specifications; + +namespace MyProject +{ + public class Age18PlusCustomerSpecification : Specification + { + public override Expression> ToExpression() + { + return c => c.Age >= 18; + } + } +} +```` + +You simply define a lambda [Expression](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions) to define a specification. + +> Instead, you can directly implement the `ISpecification` interface, but the `Specification` base class much simplifies it. + +## Using the Specifications + +There are two common use cases of the specifications. + +### IsSatisfiedBy + +`IsSatisfiedBy` method can be used to check if a single object satisfies the specification. + +**Example: Throw exception if the customer doesn't satisfy the age specification** + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace MyProject +{ + public class CustomerService : ITransientDependency + { + public async Task BuyAlcohol(Customer customer) + { + if (!new Age18PlusCustomerSpecification().IsSatisfiedBy(customer)) + { + throw new Exception( + "This customer doesn't satisfy the Age specification!" + ); + } + + //TODO... + } + } +} +```` + +### ToExpression & Repositories + +`ToExpression()` method can be used to use the specification as Expression. In this way, you can use a specification to **filter entities while querying from the database**. + +````csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; + +namespace MyProject +{ + public class CustomerManager : DomainService, ITransientDependency + { + private readonly IRepository _customerRepository; + + public CustomerManager(IRepository customerRepository) + { + _customerRepository = customerRepository; + } + + public async Task> GetCustomersCanBuyAlcohol() + { + var query = _customerRepository.Where( + new Age18PlusCustomerSpecification().ToExpression() + ); + + return await AsyncExecuter.ToListAsync(query); + } + } +} +```` + +> Specifications are correctly translated to SQL/Database queries and executed efficiently in the DBMS side. While it is not related to the Specifications, see the [Repositories](Repositories.md) document if you want to know more about the `AsyncExecuter`. + +Actually, using the `ToExpression()` method is not necessary since the specifications are automatically casted to Expressions. This would also work: + +````csharp +var query = _customerRepository.Where( + new Age18PlusCustomerSpecification() +); +```` + +## Composing the Specifications + +One powerful feature of the specifications is that they are composable with `And`, `Or`, `Not` and `AndNot` extension methods. + +Assume that you have another specification as defined below: + +```csharp +using System; +using System.Linq.Expressions; +using Volo.Abp.Specifications; + +namespace MyProject +{ + public class PremiumCustomerSpecification : Specification + { + public override Expression> ToExpression() + { + return (customer) => (customer.Balance >= 100000); + } + } +} +``` + +You can combine the `PremiumCustomerSpecification` with the `Age18PlusCustomerSpecification` to query the count of premium adult customers as shown below: + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; +using Volo.Abp.Specifications; + +namespace MyProject +{ + public class CustomerManager : DomainService, ITransientDependency + { + private readonly IRepository _customerRepository; + + public CustomerManager(IRepository customerRepository) + { + _customerRepository = customerRepository; + } + + public async Task GetAdultPremiumCustomerCountAsync() + { + return await _customerRepository.CountAsync( + new Age18PlusCustomerSpecification() + .And(new PremiumCustomerSpecification()).ToExpression() + ); + } + } +} +```` + +If you want to make this combination another reusable specification, you can create such a combination specification class deriving from the `AndSpecification`: + +````csharp +using Volo.Abp.Specifications; + +namespace MyProject +{ + public class AdultPremiumCustomerSpecification : AndSpecification + { + public AdultPremiumCustomerSpecification() + : base(new Age18PlusCustomerSpecification(), + new PremiumCustomerSpecification()) + { + } + } +} +```` + +Now, you can re-write the `GetAdultPremiumCustomerCountAsync` method as shown below: + +````csharp +public async Task GetAdultPremiumCustomerCountAsync() +{ + return await _customerRepository.CountAsync( + new AdultPremiumCustomerSpecification() + ); +} +```` + +> You see the power of the specifications with these samples. If you change the `PremiumCustomerSpecification` later, say change the balance from `100.000` to `200.000`, all the queries and combined specifications will be effected by the change. This is a good way to reduce code duplication! + +## Discussions + +While the specification pattern is older than C# lambda expressions, it's generally compared to expressions. Some developers may think it's not needed anymore and we can directly pass expressions to a repository or to a domain service as shown below: + +````csharp +var count = await _customerRepository.CountAsync(c => c.Balance > 100000 && c.Age => 18); +```` + +Since ABP's [Repository](Repositories.md) supports Expressions, this is a completely valid use. You don't have to define or use any specification in your application and you can go with expressions. + +So, what's the point of a specification? Why and when should we consider to use them? + +### When To Use? + +Some benefits of using specifications: + +- **Reusabe**: Imagine that you need the Premium Customer filter in many places in your code base. If you go with expressions and do not create a specification, what happens if you later change the "Premium Customer" definition? Say you want to change the minimum balance from $100,000 to $250,000 and add another condition to be a customer older than 3 years. If you'd used a specification, you just change a single class. If you repeated (copy/pasted) the same expression everywhere, you need to change all of them. +- **Composable**: You can combine multiple specifications to create new specifications. This is another type of reusability. +- **Named**: `PremiumCustomerSpecification` better explains the intent rather than a complex expression. So, if you have an expression that is meaningful in your business, consider using specifications. +- **Testable**: A specification is a separately (and easily) testable object. + +### When To Not Use? + +- **Non business expressions**: Do not use specifications for non business-related expressions and operations. +- **Reporting**: If you are just creating a report, do not create specifications, but directly use `IQueryable` & LINQ expressions. You can even use plain SQL, views or another tool for reporting. DDD does not necessarily care about reporting, so the way you query the underlying data store can be important from a performance perspective. \ No newline at end of file diff --git a/docs/en/Startup-Templates/Application.md b/docs/en/Startup-Templates/Application.md index 8cccf32382..81f0170e59 100644 --- a/docs/en/Startup-Templates/Application.md +++ b/docs/en/Startup-Templates/Application.md @@ -97,7 +97,7 @@ A `BookType` enum and a `BookConsts` class (which may have some constant fields #### .Domain Project -This is the domain layer of the solution. It mainly contains [entities, aggregate roots](../Entities.md), [domain services](../Domain-Services.md), [value types](../Value-Types.md), [repository interfaces](../Repositories.md) and other domain objects. +This is the domain layer of the solution. It mainly contains [entities, aggregate roots](../Entities.md), [domain services](../Domain-Services.md), [value objects](../Value-Objects.md), [repository interfaces](../Repositories.md) and other domain objects. A `Book` entity, a `BookManager` domain service and an `IBookRepository` interface are good candidates for this project. diff --git a/docs/en/Swagger.md b/docs/en/Swagger.md new file mode 100644 index 0000000000..1fda8a0cd7 --- /dev/null +++ b/docs/en/Swagger.md @@ -0,0 +1,3 @@ +# Swagger UI Integration + +TODO \ No newline at end of file diff --git a/docs/en/Testing.md b/docs/en/Testing.md index 5dafef3daf..0a617174ed 100644 --- a/docs/en/Testing.md +++ b/docs/en/Testing.md @@ -1,3 +1,673 @@ -# Testing +# Automated Testing -TODO! \ No newline at end of file +## Introduction + +ABP Framework has been designed with testability in mind. There are some different levels of automated testing; + +* **Unit Tests**: You typically test a single class (or a very few classes together). These tests will be fast. However, you generally need to deal with mocking for the dependencies of your service(s). +* **Integration Tests**: You typically test a service, but this time you don't mock the fundamental infrastructure and services to see if they properly working together. +* **UI Tests**: You test the UI of the application, just like the users interact with your application. + +### Unit Tests vs Integration Tests + +Integration tests have some significant **advantages** compared to unit tests; + +* **Easier to write** since you don't work to establish mocking and dealing with the dependencies. +* Your test code runs with all the real services and infrastructure (including database mapping and queries), so it is much closer to the **real application test**. + +While they have some drawbacks; + +* They are **slower** compared to unit tests since all the infrastructure is prepared for each test case. +* A bug in a service may make multiple test cases broken, so it may be **harder to find the real problem** in some cases. + +We suggest to go mixed: Write unit or integration test where it is necessary and you find effective to write and maintain it. + +## The Application Startup Template + +The [Application Startup Template](Startup-Templates/Application.md) comes with the test infrastructure properly installed and configured for you. + +### The Test Projects + +See the following solution structure in the Visual Studio: + +![solution-test-projects](images/solution-test-projects.png) + +There are more than one test project, organized by the layers; + +* `Domain.Tests` is used to test your Domain Layer objects (like [Domain Services](Domain-Services.md) and [Entities](Entities.md)). +* `Application.Tests` is used to test your Application Layer (like [Application Services](Application-Services.md)). +* `EntityFrameworkCore.Tests` is used to test your custom repository implementations or EF Core mappings (this project will be different if you use another [Database Provider](Data-Access.md)). +* `Web.Tests` is used to test the UI Layer (like Pages, Controllers and View Components). This project does exists only for MVC / Razor Page applications. +* `TestBase` contains some classes those are shared/used by the other projects. + +> `HttpApi.Client.ConsoleTestApp` is not an automated test application. It is an example Console Application that shows how to consume your HTTP APIs from a .NET Console Application. + +The following sections will introduce the base classes and other infrastructure included in these projects. + +### The Test Infrastructure + +The startup solution has the following libraries already installed; + +* [xUnit](https://xunit.net/) as the test framework. +* [NSubstitute](https://nsubstitute.github.io/) as the mocking library. +* [Shouldly](https://github.com/shouldly/shouldly) as the assertion library. + +While you are free to replace them with your favorite tools, this document and examples will be base on these tooling. + +## The Test Explorer + +You can use the Test Explorer to view and run the tests in Visual Studio. For other IDEs, see their own documentation. + +### Open the Test Explorer + +Open the *Test Explorer*, under the *Tests* menu, if it is not already open: + +![vs-test-explorer](images/vs-test-explorer.png) + +### Run the Tests + +Then you can click to the Run All or Run buttons to run the tests. The initial startup template has some sample tests for you: + +![vs-startup-template-tests](images/vs-startup-template-tests.png) + +### Run Tests In Parallel + +The test infrastructure is compatible to run the tests in parallel. It is **strongly suggested** to run all the tests in parallel, which is pretty faster then running them one by one. + +To enable it, click to the caret icon near to the settings (gear) button and select the *Run Tests In Parallel*. + +![vs-run-tests-in-parallel](images/vs-run-tests-in-parallel.png) + +## Unit Tests + +For Unit Tests, you don't need to much infrastructure. You typically instantiate your class and provide some pre-configured mocked objects to prepare your object to test. + +### Classes Without Dependencies + +In this simplest case, the class you want to test has no dependencies. In this case, you can directly instantiate your class, call its methods and make your assertions. + +#### Example: Testing an Entity + +Assume that you've an `Issue` [entity](Entities.md) as shown below: + +````csharp +using System; +using Volo.Abp.Domain.Entities; + +namespace MyProject.Issues +{ + public class Issue : AggregateRoot + { + public string Title { get; set; } + public string Description { get; set; } + public bool IsLocked { get; set; } + public bool IsClosed { get; private set; } + public DateTime? CloseDate { get; private set; } + + public void Close() + { + IsClosed = true; + CloseDate = DateTime.UtcNow; + } + + public void Open() + { + if (!IsClosed) + { + return; + } + + if (IsLocked) + { + throw new IssueStateException("You can not open a locked issue!"); + } + + IsClosed = true; + CloseDate = null; + } + } +} + +```` + +Notice that the `IsClosed` and `CloseDate` properties have private setters to force some business rules by using the `Open()` and `Close()` methods; + +* Whenever you close an issue, the `CloseDate` should be set to the [current time](Timing.md). +* An issue can not be re-opened if it is locked. And if it is re-opened, the `CloseDate` should be set to `null`. + +Since the `Issue` entity is a part of the Domain Layer, we should test it in the `Domain.Tests` project. Create an `Issue_Tests` class inside the `Domain.Tests` project: + +````csharp +using Shouldly; +using Xunit; + +namespace MyProject.Issues +{ + public class Issue_Tests + { + [Fact] + public void Should_Set_The_CloseDate_Whenever_Close_An_Issue() + { + // Arrange + + var issue = new Issue(); + issue.CloseDate.ShouldBeNull(); // null at the beginning + + // Act + + issue.Close(); + + // Assert + + issue.IsClosed.ShouldBeTrue(); + issue.CloseDate.ShouldNotBeNull(); + } + } +} +```` + +This test follows the AAA (Arrange-Act-Assert) pattern; + +* **Arrange** part creates an `Issue` entity and ensures the `CloseDate` is `null` at the beginning. +* **Act** part executes the method we want to test for this case. +* **Assert** part checks if the `Issue` properties are same as we expect to be. + +`[Fact]` attribute is defined by the [xUnit](https://xunit.net/) library and marks a method as a test method. `Should...` extension methods are provided by the [Shouldly](https://github.com/shouldly/shouldly) library. You can directly use the `Assert` class of the xUnit, but Shouldly makes it much comfortable and straightforward. + +When you execute the tests, you will see that is passes successfully: + +![issue-first-test](images/issue-first-test.png) + +Let's add two more test methods: + +````csharp +[Fact] +public void Should_Allow_To_ReOpen_An_Issue() +{ + // Arrange + + var issue = new Issue(); + issue.Close(); + + // Act + + issue.Open(); + + // Assert + + issue.IsClosed.ShouldBeFalse(); + issue.CloseDate.ShouldBeNull(); +} + +[Fact] +public void Should_Not_Allow_To_ReOpen_A_Locked_Issue() +{ + // Arrange + + var issue = new Issue(); + issue.Close(); + issue.IsLocked = true; + + // Act & Assert + + Assert.Throws(() => + { + issue.Open(); + }); +} +```` + +`Assert.Throws` checks if the executed code throws a matching exception. + +> See the xUnit & Shoudly documentations to learn more about these libraries. + +### Classes With Dependencies + +If your service has dependencies and you want to unit test this service, you need to mock the dependencies. + +#### Example: Testing a Domain Service + +Assume that you've an `IssueManager` [Domain Service](Domain-Services.md) that is defined as below: + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Domain.Services; + +namespace MyProject.Issues +{ + public class IssueManager : DomainService + { + public const int MaxAllowedOpenIssueCountForAUser = 3; + + private readonly IIssueRepository _issueRepository; + + public IssueManager(IIssueRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task AssignToUserAsync(Issue issue, Guid userId) + { + var issueCount = await _issueRepository.GetIssueCountOfUserAsync(userId); + + if (issueCount >= MaxAllowedOpenIssueCountForAUser) + { + throw new BusinessException( + code: "IM:00392", + message: $"You can not assign more" + + $"than {MaxAllowedOpenIssueCountForAUser} issues to a user!" + ); + } + + issue.AssignedUserId = userId; + } + } +} +```` + +`IssueManager` depends on the `IssueRepository` service, that will be mocked in this example. + +**Business Rule**: The example `AssignToUserAsync` doesn't allow to assign more than 3 (`MaxAllowedOpenIssueCountForAUser` constant) issues to a user. If you want to assign an issue in this case, you first need to unassign an existing issue. + +The test case below tries to make a valid assignment: + +````csharp +using System; +using System.Threading.Tasks; +using NSubstitute; +using Shouldly; +using Volo.Abp; +using Xunit; + +namespace MyProject.Issues +{ + public class IssueManager_Tests + { + [Fact] + public async Task Should_Assign_An_Issue_To_A_User() + { + // Arrange + + var userId = Guid.NewGuid(); + + var fakeRepo = Substitute.For(); + fakeRepo.GetIssueCountOfUserAsync(userId).Returns(1); + + var issueManager = new IssueManager(fakeRepo); + + var issue = new Issue(); + + // Act + + await issueManager.AssignToUserAsync(issue, userId); + + //Assert + + issue.AssignedUserId.ShouldBe(userId); + await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId); + } + } +} +```` + +* `Substitute.For` creates a mock (fake) object that is passed into the `IssueManager` constructor. +* `fakeRepo.GetIssueCountOfUserAsync(userId).Returns(1)` ensures that the `GetIssueCountOfUserAsync` method of the repository returns `1`. +* `issueManager.AssignToUserAsync` doesn't throw any exception since the repository returns `1` for the currently assigned issue count. +* `issue.AssignedUserId.ShouldBe(userId);` line checks if the `AssignedUserId` has the correct value. +* `await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId);` checks if the `IssueManager` called the `GetIssueCountOfUserAsync` method exactly one time. + +Let's add a second test to see if it prevents to assign issues to a user more than the allowed count: + +````csharp +[Fact] +public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit() +{ + // Arrange + + var userId = Guid.NewGuid(); + + var fakeRepo = Substitute.For(); + fakeRepo + .GetIssueCountOfUserAsync(userId) + .Returns(IssueManager.MaxAllowedOpenIssueCountForAUser); + + var issueManager = new IssueManager(fakeRepo); + + // Act & Assert + + var issue = new Issue(); + + await Assert.ThrowsAsync(async () => + { + await issueManager.AssignToUserAsync(issue, userId); + }); + + issue.AssignedUserId.ShouldBeNull(); + await fakeRepo.Received(1).GetIssueCountOfUserAsync(userId); +} +```` + +> For more information on the mocking, see the [NSubstitute](https://nsubstitute.github.io/) documentation. + +It is relatively easy to mock a single dependency. But, when your dependencies grow, it gets harder to setup the test objects and mock all the dependencies. See the *Integration Tests* section that doesn't require mocking the dependencies. + +### Tip: Share the Test Class Constructor + +[xUnit](https://xunit.net/) creates a **new test class instance** (`IssueManager_Tests` for this example) for each test method. So, you can move some *Arrange* code into the constructor to reduce the code duplication. The constructor will be executed for each test case and doesn't affect each other, even if they work in parallel. + +**Example: Refactor the `IssueManager_Tests` to reduce the code duplication** + +````csharp +using System; +using System.Threading.Tasks; +using NSubstitute; +using Shouldly; +using Volo.Abp; +using Xunit; + +namespace MyProject.Issues +{ + public class IssueManager_Tests + { + private readonly Guid _userId; + private readonly IIssueRepository _fakeRepo; + private readonly IssueManager _issueManager; + private readonly Issue _issue; + + public IssueManager_Tests() + { + _userId = Guid.NewGuid(); + _fakeRepo = Substitute.For(); + _issueManager = new IssueManager(_fakeRepo); + _issue = new Issue(); + } + + [Fact] + public async Task Should_Assign_An_Issue_To_A_User() + { + // Arrange + _fakeRepo.GetIssueCountOfUserAsync(_userId).Returns(1); + + // Act + await _issueManager.AssignToUserAsync(_issue, _userId); + + //Assert + _issue.AssignedUserId.ShouldBe(_userId); + await _fakeRepo.Received(1).GetIssueCountOfUserAsync(_userId); + } + + [Fact] + public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit() + { + // Arrange + _fakeRepo + .GetIssueCountOfUserAsync(_userId) + .Returns(IssueManager.MaxAllowedOpenIssueCountForAUser); + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + await _issueManager.AssignToUserAsync(_issue, _userId); + }); + + _issue.AssignedUserId.ShouldBeNull(); + await _fakeRepo.Received(1).GetIssueCountOfUserAsync(_userId); + } + } +} +```` + +> Keep your test code clean to create a maintainable test suite. + +## Integration Tests + +> You can follow the [web application development tutorial](Tutorials/Part-1.md) to learn developing a full stack application, including the integration tests. + +### The Integration Test Infrastructure + +ABP Provides a complete infrastructure to write integration tests. All the ABP infrastructure and services will perform in your tests. The application startup template comes with the necessary infrastructure pre-configured for you; + +#### The Database + +The startup template is configured to use **in-memory SQLite** database for the EF Core (for MongoDB, it uses [Mongo2Go](https://github.com/Mongo2Go/Mongo2Go) library). So, all the configuration and queries are performed against a real database and you can even test database transactions. + +Using in-memory SQLite database has two main advantages; + +* It is faster compared to an external DBMS. +* It create a **new fresh database** for each test case, so tests doesn't affect each other. + +> **Tip**: Do not use EF Core's In-Memory database for advanced integration tests. It is not a real DBMS and has many differences in details. For example, it doesn't support transaction and rollback scenarios, so you can't truly test the failing scenarios. On the other hand, In-Memory SQLite is a real DBMS and supports the fundamental SQL database features. + +### The Seed Data + +Writing tests against an empty database is not practical. In most cases, you need to some initial data in the database. For example, if you write a test class that query, update and delete the Products, it would be helpful to have a few products in the database before executing the test case. + +ABP's [Data Seeding](Data-Seeding.md) system is a powerful way to seed the initial data. The application startup template has a *YourProject*TestDataSeedContributor class in the `.TestBase` project. You can fill it to have an initial data that you can use for each test method. + +**Example: Create some Issues as the seed data** + +````csharp +using System.Threading.Tasks; +using MyProject.Issues; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; + +namespace MyProject +{ + public class MyProjectTestDataSeedContributor + : IDataSeedContributor, ITransientDependency + { + private readonly IIssueRepository _issueRepository; + + public MyProjectTestDataSeedContributor(IIssueRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task SeedAsync(DataSeedContext context) + { + await _issueRepository.InsertAsync( + new Issue + { + Title = "Test issue one", + Description = "Test issue one description", + AssignedUserId = TestData.User1Id + }); + + await _issueRepository.InsertAsync( + new Issue + { + Title = "Test issue two", + Description = "Test issue two description", + AssignedUserId = TestData.User1Id + }); + + await _issueRepository.InsertAsync( + new Issue + { + Title = "Test issue three", + Description = "Test issue three description", + AssignedUserId = TestData.User1Id + }); + + await _issueRepository.InsertAsync( + new Issue + { + Title = "Test issue four", + Description = "Test issue four description", + AssignedUserId = TestData.User2Id + }); + } + } +} +```` + +Also created a static class to store the User `Ids`: + +````csharp +using System; + +namespace MyProject +{ + public static class TestData + { + public static Guid User1Id = Guid.Parse("41951813-5CF9-4204-8B18-CD765DBCBC9B"); + public static Guid User2Id = Guid.Parse("2DAB4460-C21B-4925-BF41-A52750A9B999"); + } +} +```` + +In this way, we can use these known Issues and the User `Id`s to perform the tests. + +### Example: Testing a Domain Service + +`AbpIntegratedTest` class (defined in the [Volo.Abp.TestBase](https://www.nuget.org/packages/Volo.Abp.TestBase) package) is used to write tests integrated to the ABP Framework. `T` is the Type of the root module to setup and initialize the application. + +The application startup template has base classes in each test project, so you can derive from these base classes to make it easier. + +See the `IssueManager` tests are re-written as integration tests + +````csharp +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp; +using Xunit; + +namespace MyProject.Issues +{ + public class IssueManager_Integration_Tests : MyProjectDomainTestBase + { + private readonly IssueManager _issueManager; + private readonly Issue _issue; + + public IssueManager_Integration_Tests() + { + _issueManager = GetRequiredService(); + _issue = new Issue + { + Title = "Test title", + Description = "Test description" + }; + } + + [Fact] + public async Task Should_Not_Allow_To_Assign_Issues_Over_The_Limit() + { + // Act & Assert + await Assert.ThrowsAsync(async () => + { + await _issueManager.AssignToUserAsync(_issue, TestData.User1Id); + }); + + _issue.AssignedUserId.ShouldBeNull(); + } + + [Fact] + public async Task Should_Assign_An_Issue_To_A_User() + { + // Act + await _issueManager.AssignToUserAsync(_issue, TestData.User2Id); + + //Assert + _issue.AssignedUserId.ShouldBe(TestData.User2Id); + } + } +} +```` + +* First test method assigns the issue to the User 1, which has already assigned to 3 issues in the Data Seed code. So, it throws a `BusinessException`. +* Second test method assigns the issue to User 2, which has only 1 issue assigned. So, the method succeeds. + +This class typically locates in the `.Domain.Tests` project since it tests a class located in the `.Domain` project. It is derived from the `MyProjectDomainTestBase` which is already configured to properly run the tests. + +Writing such an integration test class is very straightforward. Another benefit is that you won't need to change the test class later when you add another dependency to the `IssueManager` class. + +### Example: Testing an Application Service + +Testing an [Application Service](Application-Services.md) is not so different. Assume that you've created an `IssueAppService` as defined below: + +````csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace MyProject.Issues +{ + public class IssueAppService : ApplicationService, IIssueAppService + { + private readonly IIssueRepository _issueRepository; + + public IssueAppService(IIssueRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task> GetListAsync() + { + var issues = await _issueRepository.GetListAsync(); + + return ObjectMapper.Map, List>(issues); + } + } +} +```` + +*(assuming you've also defined the `IIssueAppService` and `IssueDto` and created the [object mapping](Object-To-Object-Mapping.md) between `Issue` and the `IssueDto`)* + +Now, you can write a test class inside the `.Application.Tests` project: + +````csharp +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace MyProject.Issues +{ + public class IssueAppService_Tests : MyProjectApplicationTestBase + { + private readonly IIssueAppService _issueAppService; + + public IssueAppService_Tests() + { + _issueAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_All_Issues() + { + //Act + var issueDtos = await _issueAppService.GetListAsync(); + + //Assert + issueDtos.Count.ShouldBeGreaterThan(0); + } + } +} +```` + +It's that simple. This test method tests everything, including the application service, EF Core mapping, object to object mapping and the repository implementation. In this way, you can fully test the Application Later and the Domain Layer of your solution. + +## UI Tests + +In general, there are two types of UI Tests; + +### Non Visual Tests + +Such tests completely depends on your UI Framework choice; + +* For an MVC / Razor Pages UI, you typically make request to the server, get some HTML and test if some expected DOM elements exist in the returned result. +* Angular has its own infrastructure and practices to test the components, views and services. + +See the following documents to learn Non Visual UI Testing; + +* [Testing in ASP.NET Core MVC / Razor Pages](UI/AspNetCore/Testing.md) +* [Testing in Angular](UI/Angular/Testing.md) +* [Testing in Blazor](UI/Blazor/Testing.md) + +### Visual Tests + +Visual Tests are used to interact with the application UI just like a real user does. It fully tests the application, including the visual appearance of the pages and components. + +Visual UI Testing is out of the scope for the ABP Framework. There are a lot of tooling in the industry (like [Selenium](https://www.selenium.dev/)) that you can use to test your application's UI. \ No newline at end of file diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index 07f3b141ab..bdea7cd00b 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -187,9 +187,9 @@ var result = await _templateRenderer.RenderAsync( In this case, we haven't created a model class, but created an anonymous object as the model. -### PascalCase vs camelCase +### PascalCase vs snake_case -PascalCase property names (like `UserName`) is used as camelCase (like `userName`) in the templates. +PascalCase property names (like `UserName`) is used as snake_case (like `user_name`) in the templates. ## Localization @@ -423,7 +423,7 @@ This example simply adds a header and footer to the template and renders the con **3)** Configure the embedded resources in the `.csproj` file * Add [Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet package to the project. -* Add `true` into the `...` section of your `.csproj` file. +* Add `true` into the `...` section of your `.csproj` file. * Add the following code into your `.csproj` file: ````xml diff --git a/docs/en/Themes/Basic.md b/docs/en/Themes/Basic.md index a0165c803a..819138de5f 100644 --- a/docs/en/Themes/Basic.md +++ b/docs/en/Themes/Basic.md @@ -1,3 +1,2 @@ -## Basic Theme +This document has been moved to [here](../UI/AspNetCore/Basic-Theme.md). -TODO \ No newline at end of file diff --git a/docs/en/Tutorials/Part-10.md b/docs/en/Tutorials/Part-10.md index 53f19d93dd..a9de3a062e 100644 --- a/docs/en/Tutorials/Part-10.md +++ b/docs/en/Tutorials/Part-10.md @@ -10,7 +10,7 @@ In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: -* **{{DB_Value}}** as the ORM provider. +* **{{DB_Value}}** as the ORM provider. * **{{UI_Value}}** as the UI Framework. This tutorial is organized as the following parts; @@ -50,7 +50,7 @@ public Guid AuthorId { get; set; } {{if DB=="EF"}} -> In this tutorial, we preferred to not add a **navigation property** to the `Author` entity from the `Book` class (like `public Author Author { get; set; }`). This is due to follow the DDD best practices (rule: refer to other aggregates only by id). However, you can add such a navigation property and configure it for the EF Core. In this way, you don't need to write join queries while getting books with their authors (just like we will done below) which makes your application code simpler. +> In this tutorial, we preferred to not add a **navigation property** to the `Author` entity from the `Book` class (like `public Author Author { get; set; }`). This is due to follow the DDD best practices (rule: refer to other aggregates only by id). However, you can add such a navigation property and configure it for the EF Core. In this way, you don't need to write join queries while getting books with their authors (like we will done below) which makes your application code simpler. {{end}} @@ -78,7 +78,7 @@ builder.Entity(b => b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema); b.ConfigureByConvention(); //auto configure for the base class props b.Property(x => x.Name).IsRequired().HasMaxLength(128); - + // ADD THE MAPPING FOR THE RELATION b.HasOne().WithMany().HasForeignKey(x => x.AuthorId).IsRequired(); }); @@ -384,8 +384,8 @@ namespace Acme.BookStore.Books return bookDto; } - public override async Task> - GetListAsync(PagedAndSortedResultRequestDto input) + public override async Task> GetListAsync( + PagedAndSortedResultRequestDto input) { await CheckGetListPolicyAsync(); @@ -485,7 +485,7 @@ namespace Acme.BookStore.Books DeletePolicyName = BookStorePermissions.Books.Create; } - public override async Task GetAsync(Guid id) + public async override Task GetAsync(Guid id) { await CheckGetPolicyAsync(); @@ -498,7 +498,7 @@ namespace Acme.BookStore.Books return bookDto; } - public override async Task> + public async override Task> GetListAsync(PagedAndSortedResultRequestDto input) { await CheckGetListPolicyAsync(); @@ -524,7 +524,7 @@ namespace Acme.BookStore.Books var authorDictionary = await GetAuthorDictionaryAsync(books); //Set AuthorName for the DTOs - bookDtos.ForEach(bookDto => bookDto.AuthorName = + bookDtos.ForEach(bookDto => bookDto.AuthorName = authorDictionary[bookDto.AuthorId].Name); //Get the total count with another query (required for the paging) @@ -622,7 +622,7 @@ namespace Acme.BookStore.Books result.Items.ShouldContain(b => b.Name == "1984" && b.AuthorName == "George Orwell"); } - + [Fact] public async Task Should_Create_A_Valid_Book() { @@ -645,7 +645,7 @@ namespace Acme.BookStore.Books result.Id.ShouldNotBe(Guid.Empty); result.Name.ShouldBe("New test book 42"); } - + [Fact] public async Task Should_Not_Create_A_Book_Without_Name() { @@ -911,6 +911,17 @@ You can run the application and try to create a new book or update an existing b {{else if UI=="NG"}} +### Service Proxy Generation + +Since the HTTP APIs have been changed, you need to update Angular client side [service proxies](../UI/Angular/Service-Proxies.md). Before running `generate-proxy` command, your host must be up and running. + +Run the following command in the `angular` folder (you may need to stop the angular application): + +```bash +abp generate-proxy +``` +This command will update the service proxy files under the `/src/app/proxy/` folder. + ### The Book List Book list page change is trivial. Open the `/src/app/book/book.component.html` and add the following column definition between the `Name` and `Type` columns: @@ -1082,36 +1093,37 @@ Add the following field to the `@code` section of the `Books.razor` file: IReadOnlyList authorList = Array.Empty(); ```` -And fill it in the `OnInitializedAsync` method, by adding the following code to the end of the method: +Override the `OnInitializedAsync` method and adding the following code: ````csharp -authorList = (await AppService.GetAuthorLookupAsync()).Items; +protected override async Task OnInitializedAsync() +{ + await base.OnInitializedAsync(); + authorList = (await AppService.GetAuthorLookupAsync()).Items; +} ```` +* It is essential to call the `base.OnInitializedAsync()` since `AbpCrudPageBase` has some initialization code to be executed. + The final `@code` block should be the following: ````csharp @code { - bool canCreateBook; - bool canEditBook; - bool canDeleteBook; - //ADDED A NEW FIELD IReadOnlyList authorList = Array.Empty(); + public Books() // Constructor + { + CreatePolicyName = BookStorePermissions.Books.Create; + UpdatePolicyName = BookStorePermissions.Books.Edit; + DeletePolicyName = BookStorePermissions.Books.Delete; + } + + //GET AUTHORS ON INITIALIZATION protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - - canCreateBook = await - AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create); - canEditBook = await - AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Edit); - canDeleteBook = await - AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Delete); - - //GET AUTHORS authorList = (await AppService.GetAuthorLookupAsync()).Items; } } @@ -1164,4 +1176,4 @@ Add the following `Field` definition into the `ModalBody` of the *Edit* modal, a That's all. We are reusing the `authorList` defined for the *Create* modal. -{{end}} \ No newline at end of file +{{end}} diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index f4fcf61e25..7bc42b1fc9 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/docs/en/Tutorials/Part-2.md @@ -590,7 +590,6 @@ Open the `Books.razor` and replace the content as the following: ````xml @page "/books" @using Volo.Abp.Application.Dtos -@using Volo.Abp.BlazoriseUI @using Acme.BookStore.Books @using Acme.BookStore.Localization @using Microsoft.Extensions.Localization @@ -620,14 +619,14 @@ Open the `Books.razor` and replace the content as the following: @context.PublishDate.ToShortDateString() ` section with the following ````xml - - + +

@L["Books"]

- - - - + +
@@ -1196,48 +1194,67 @@ Now, we can add a modal that will be opened when we click to the button. Open the `Books.razor` and add the following code to the end of the page: ````xml - + - - @L["NewBook"] - - - - - @L["Name"] - - - - @L["Type"] - - - - @L["PublishDate"] - - - - @L["Price"] - - - - - - - +
+ + @L["NewBook"] + + + + + + + @L["Name"] + + + + + + + + + @L["Type"] + + + + @L["PublishDate"] + + + + @L["Price"] + + + + + + + + +
```` +This code requires a service; Inject the `AbpBlazorMessageLocalizerHelper` at the top of the file, just before the `@inherits...` line: + +````csharp +@inject AbpBlazorMessageLocalizerHelper LH +```` + +* 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. That's all. Run the application and try to add a new book: @@ -1250,78 +1267,81 @@ Editing a books is similar to the creating a new book. ### Actions Dropdown -Open the `Books.razor` and add the following `DataGridColumn` section inside the `DataGridColumns` as the first item: +Open the `Books.razor` and add the following `DataGridEntityActionsColumn` section inside the `DataGridColumns` as the first item: ````xml - + - - - @L["Actions"] - - - - @L["Edit"] - - - + + + - + ```` -* `OpenEditModalAsync` is defined in the base class which takes the `Id` of the entity (book) to edit. +* `OpenEditModalAsync` is defined in the base class which takes the entity (book) to edit. -This adds an "Actions" dropdown to all the books inside the `DataGrid` with an `Edit` action: +`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: -![blazor-edit-book-action](images/blazor-edit-book-action.png) +![blazor-edit-book-action](images/blazor-edit-book-action-2.png) ### Edit Modal We can now define a modal to edit the book. Add the following code to the end of the `Books.razor` page: ````xml - + - - @EditingEntity.Name - - - - - @L["Name"] - - - - @L["Type"] - - - - @L["PublishDate"] - - - - @L["Price"] - - - - - - - +
+ + @EditingEntity.Name + + + + + + + @L["Name"] + + + + + + + + + @L["Type"] + + + + @L["PublishDate"] + + + + @L["Price"] + + + + + + + + +
```` @@ -1356,17 +1376,26 @@ You can now run the application and try to edit a book. ![blazor-edit-book-modal](images/blazor-edit-book-modal.png) +> Tip: Try to leave the *Name* field empty and submit the form to show the validation error message. + ## Deleting a Book -Open the `Books.razor` page and add the following `DropdownItem` under the "Edit" action inside the "Actions" `DropdownMenu`: +Open the `Books.razor` page and add the following `EntityAction` under the "Edit" action inside the `EntityActions`: ````xml - - @L["Delete"] - + ```` -* `DeleteEntityAsync` is defined in the base class. +* `DeleteEntityAsync` is defined in the base class that deletes the entity by performing a call to the server. +* `ConfirmationMessage` is a callback to show a confirmation message before executing the action. +* `GetDeleteConfirmationMessage` is defined in the base class. You can override this method (or pass another value to the `ConfirmationMessage` parameter) to customize the localization message. + +The "Actions" button becomes a dropdown since it has two actions now: + +![blazor-edit-book-action](images/blazor-delete-book-action.png) Run the application and try to delete a book. @@ -1377,26 +1406,22 @@ Here the complete code to create the book management CRUD page, that has been de ````xml @page "/books" @using Volo.Abp.Application.Dtos -@using Volo.Abp.BlazoriseUI @using Acme.BookStore.Books @using Acme.BookStore.Localization @using Microsoft.Extensions.Localization @inject IStringLocalizer L +@inject AbpBlazorMessageLocalizerHelper LH @inherits AbpCrudPageBase - - + +

@L["Books"]

- - + - + Clicked="OpenCreateModalAsync">@L["NewBook"]
@@ -1404,31 +1429,24 @@ Here the complete code to create the book management CRUD page, that has been de - + - - - @L["Actions"] - - - - @L["Edit"] - - - @L["Delete"] - - - + + + + - + @@ -1436,7 +1454,7 @@ Here the complete code to create the book management CRUD page, that has been de Field="@nameof(BookDto.Type)" Caption="@L["Type"]"> - @L[$"Enum:BookType:{(int)context.Type}"] + @L[$"Enum:BookType:{(int) context.Type}"]
- + - - @L["NewBook"] - - - - - @L["Name"] - - - - @L["Type"] - - - - @L["PublishDate"] - - - - @L["Price"] - - - - - - - +
+ + @L["NewBook"] + + + + + + + @L["Name"] + + + + + + + + + @L["Type"] + + + + @L["PublishDate"] + + + + @L["Price"] + + + + + + + + +
- + - - @EditingEntity.Name - - - - - @L["Name"] - - - - @L["Type"] - - - - @L["PublishDate"] - - - - @L["Price"] - - - - - - - +
+ + @EditingEntity.Name + + + + + + + @L["Name"] + + + + + + + + + @L["Type"] + + + + @L["PublishDate"] + + + + @L["Price"] + + + + + + + + +
```` diff --git a/docs/en/Tutorials/Part-4.md b/docs/en/Tutorials/Part-4.md index d9c000be5c..2262598544 100644 --- a/docs/en/Tutorials/Part-4.md +++ b/docs/en/Tutorials/Part-4.md @@ -53,7 +53,7 @@ This part covers the **server side** tests. There are several test projects in t Each project is used to test the related project. Test projects use the following libraries for testing: * [Xunit](https://xunit.github.io/) as the main test framework. -* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as the assertion library. +* [Shoudly](https://github.com/shouldly/shouldly) as the assertion library. * [NSubstitute](http://nsubstitute.github.io/) as the mocking library. {{if DB=="EF"}} @@ -126,7 +126,7 @@ public async Task Should_Create_A_Valid_Book() { Name = "New test book 42", Price = 10, - PublishDate = System.DateTime.Now, + PublishDate = DateTime.Now, Type = BookType.ScienceFiction } ); @@ -208,7 +208,7 @@ namespace Acme.BookStore.Books { Name = "New test book 42", Price = 10, - PublishDate = System.DateTime.Now, + PublishDate = DateTime.Now, Type = BookType.ScienceFiction } ); diff --git a/docs/en/Tutorials/Part-5.md b/docs/en/Tutorials/Part-5.md index a293ec586e..29374419cb 100644 --- a/docs/en/Tutorials/Part-5.md +++ b/docs/en/Tutorials/Part-5.md @@ -70,7 +70,7 @@ namespace Acme.BookStore.Permissions } ```` -This is a hierarchical way of defining permission names. For example, "create book" permission name was defined as `BookStore.Books.Create`. +This is a hierarchical way of defining permission names. For example, "create book" permission name was defined as `BookStore.Books.Create`. ABP doesn't force you to a structure, but we find this way useful. ### Permission Definitions @@ -423,13 +423,13 @@ Open the `/src/app/book/book.component.html` file and replace the create button ````html - ```` -* Just added `abpPermission="BookStore.Books.Create"` that hides the button if the current user has no permission. +* Just added `*abpPermission="'BookStore.Books.Create'"` that hides the button if the current user has no permission. ### Hide the Edit and Delete Actions @@ -443,18 +443,18 @@ Open the `/src/app/book/book.component.html` file and replace the edit and delet ````html - - ```` -* Added `abpPermission="BookStore.Books.Edit"` that hides the edit action if the current user has no editing permission. -* Added `abpPermission="BookStore.Books.Delete"` that hides the delete action if the current user has no delete permission. +* Added `*abpPermission="'BookStore.Books.Edit'"` that hides the edit action if the current user has no editing permission. +* Added `*abpPermission="'BookStore.Books.Delete'"` that hides the delete action if the current user has no delete permission. {{else if UI == "Blazor"}} @@ -476,29 +476,29 @@ Adding this attribute prevents to enter this page if the current hasn't logged i The book management page has a *New Book* button and *Edit* and *Delete* actions for each book. We should hide these buttons/actions if the current user has not granted for the related permissions. -#### Get the Permissions On Initialization +The base `AbpCrudPageBase` class already has the necessary functionality for these kind of operations. + +#### Set the Policy (Permission) Names Add the following code block to the end of the `Books.razor` file: ````csharp @code { - bool canCreateBook; - bool canEditBook; - bool canDeleteBook; - - protected override async Task OnInitializedAsync() + public Books() // Constructor { - await base.OnInitializedAsync(); - - canCreateBook =await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create); - canEditBook = await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Edit); - canDeleteBook = await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Delete); + CreatePolicyName = BookStorePermissions.Books.Create; + UpdatePolicyName = BookStorePermissions.Books.Edit; + DeletePolicyName = BookStorePermissions.Books.Delete; } } ```` -We will use these `bool` fields to check the permissions. `AuthorizationService` comes from the base class as an injected property. +The base `AbpCrudPageBase` class automatically checks these permissions on the related operations. It also defines the given properties for us if we need to check them manually: + +* `HasCreatePermission`: True, if the current user has permission to create the entity. +* `HasUpdatePermission`: True, if the current user has permission to edit/update the entity. +* `HasDeletePermission`: True, if the current user has permission to delete the entity. > **Blazor Tip**: While adding the C# code into a `@code` block is fine for small code parts, it is suggested to use the code behind approach to develop a more maintainable code base when the code block becomes longer. We will use this approach for the authors part. @@ -507,32 +507,31 @@ We will use these `bool` fields to check the permissions. `AuthorizationService` Wrap the *New Book* button by an `if` block as shown below: ````xml -@if (canCreateBook) +@if (HasCreatePermission) { + Clicked="OpenCreateModalAsync">@L["NewBook"] } ```` #### Hide the Edit/Delete Actions -As similar to the *New Book* button, we can use `if` blocks to conditionally show/hide the *Edit* and *Delete* actions: +`EntityAction` component defines `RequiredPolicy` attribute (parameter) to conditionally show the action based on the user permissions. + +Update the `EntityActions` section as shown below: ````xml -@if (canEditBook) -{ - - @L["Edit"] - -} -@if (canDeleteBook) -{ - - @L["Delete"] - -} + + + + ```` #### About the Permission Caching @@ -587,54 +586,39 @@ if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) } ```` -You also need to add `async` keyword to the `ConfigureMenuAsync` method and re-arrange the return values. The final `BookStoreMenuContributor` class should be the following: +You also need to add `async` keyword to the `ConfigureMenuAsync` method and re-arrange the return value. The final `ConfigureMainMenuAsync` method should be the following: ````csharp -using System.Threading.Tasks; -using Acme.BookStore.Localization; -using Acme.BookStore.Permissions; -using Volo.Abp.UI.Navigation; - -namespace Acme.BookStore.Blazor +private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) { - public class BookStoreMenuContributor : IMenuContributor - { - public async Task ConfigureMenuAsync(MenuConfigurationContext context) - { - if(context.Menu.DisplayName != StandardMenus.Main) - { - return; - } + var l = context.GetLocalizer(); - var l = context.GetLocalizer(); - - context.Menu.Items.Insert( - 0, - new ApplicationMenuItem( - "BookStore.Home", - l["Menu:Home"], - "/", - icon: "fas fa-home" - ) - ); + context.Menu.Items.Insert( + 0, + new ApplicationMenuItem( + "BookStore.Home", + l["Menu:Home"], + "/", + icon: "fas fa-home" + ) + ); - var bookStoreMenu = new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ); + var bookStoreMenu = new ApplicationMenuItem( + "BooksStore", + l["Menu:BookStore"], + icon: "fa fa-book" + ); - context.Menu.AddItem(bookStoreMenu); + context.Menu.AddItem(bookStoreMenu); - if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) - { - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/books" - )); - } - } + //CHECK the PERMISSION + if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) + { + bookStoreMenu.AddItem(new ApplicationMenuItem( + "BooksStore.Books", + l["Menu:Books"], + url: "/books" + )); } } ```` diff --git a/docs/en/Tutorials/Part-7.md b/docs/en/Tutorials/Part-7.md index 5cae52a4b1..97d28691ae 100644 --- a/docs/en/Tutorials/Part-7.md +++ b/docs/en/Tutorials/Part-7.md @@ -150,7 +150,7 @@ namespace Acme.BookStore.Authors } ```` -* Inherited from the `EfCoreAuthorRepository`, so it inherits the standard repository method implementations. +* Inherited from the `EfCoreRepository`, so it inherits the standard repository method implementations. * `WhereIf` is a shortcut extension method of the ABP Framework. It adds the `Where` condition only if the first condition meets (it filters by name, only if the filter was provided). You could do the same yourself, but these type of shortcut methods makes our life easier. * `sorting` can be a string like `Name`, `Name ASC` or `Name DESC`. It is possible by using the [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) NuGet package. @@ -211,7 +211,7 @@ namespace Acme.BookStore.Authors } ``` -* Inherited from the `MongoDbAuthorRepository`, so it inherits the standard repository method implementations. +* Inherited from the `MongoDbRepository`, so it inherits the standard repository method implementations. * `WhereIf` is a shortcut extension method of the ABP Framework. It adds the `Where` condition only if the first condition meets (it filters by name, only if the filter was provided). You could do the same yourself, but these type of shortcut methods makes our life easier. * `sorting` can be a string like `Name`, `Name ASC` or `Name DESC`. It is possible by using the [System.Linq.Dynamic.Core](https://www.nuget.org/packages/System.Linq.Dynamic.Core) NuGet package. @@ -221,4 +221,4 @@ namespace Acme.BookStore.Authors ## The Next Part -See the [next part](Part-8.md) of this tutorial. \ No newline at end of file +See the [next part](Part-8.md) of this tutorial. diff --git a/docs/en/Tutorials/Part-9.md b/docs/en/Tutorials/Part-9.md index 374efff2b4..a1b4b03e21 100644 --- a/docs/en/Tutorials/Part-9.md +++ b/docs/en/Tutorials/Part-9.md @@ -843,15 +843,8 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the `Acme.BookStor ````xml @page "/authors" @using Acme.BookStore.Authors -@using Acme.BookStore.Localization -@using Microsoft.AspNetCore.Authorization -@using Microsoft.Extensions.Localization -@using Volo.Abp.ObjectMapping +@inherits BookStoreComponentBase @inject IAuthorAppService AuthorAppService -@inject IStringLocalizer L -@inject IAuthorizationService AuthorizationService -@inject IUiMessageService UiMessageService -@inject IObjectMapper ObjectMapper @@ -1048,10 +1041,10 @@ namespace Acme.BookStore.Blazor.Pages { CanCreateAuthor = await AuthorizationService .IsGrantedAsync(BookStorePermissions.Authors.Create); - + CanEditAuthor = await AuthorizationService .IsGrantedAsync(BookStorePermissions.Authors.Edit); - + CanDeleteAuthor = await AuthorizationService .IsGrantedAsync(BookStorePermissions.Authors.Delete); } @@ -1105,7 +1098,7 @@ namespace Acme.BookStore.Blazor.Pages private async Task DeleteAuthorAsync(AuthorDto author) { var confirmMessage = L["AuthorDeletionConfirmationMessage", author.Name]; - if (!await UiMessageService.ConfirmAsync(confirmMessage)) + if (!await Message.Confirm(confirmMessage)) { return; } @@ -1195,4 +1188,4 @@ That's all! This is a fully working CRUD page, you can create, edit and delete t ## The Next Part -See the [next part](Part-10.md) of this tutorial. \ No newline at end of file +See the [next part](Part-10.md) of this tutorial. diff --git a/docs/en/Tutorials/images/blazor-bookstore-book-list.png b/docs/en/Tutorials/images/blazor-bookstore-book-list.png index 91450f47c2..18bb26ccb1 100644 Binary files a/docs/en/Tutorials/images/blazor-bookstore-book-list.png and b/docs/en/Tutorials/images/blazor-bookstore-book-list.png differ diff --git a/docs/en/Tutorials/images/blazor-delete-book-action.png b/docs/en/Tutorials/images/blazor-delete-book-action.png new file mode 100644 index 0000000000..f2b0b83f30 Binary files /dev/null and b/docs/en/Tutorials/images/blazor-delete-book-action.png differ diff --git a/docs/en/Tutorials/images/blazor-edit-book-action-2.png b/docs/en/Tutorials/images/blazor-edit-book-action-2.png new file mode 100644 index 0000000000..70856ab325 Binary files /dev/null and b/docs/en/Tutorials/images/blazor-edit-book-action-2.png differ diff --git a/docs/en/UI/Angular/Authorization.md b/docs/en/UI/Angular/Authorization.md new file mode 100644 index 0000000000..75d6107341 --- /dev/null +++ b/docs/en/UI/Angular/Authorization.md @@ -0,0 +1,28 @@ +# Authorization in Angular UI + +OAuth is preconfigured in Angular application templates. So, when you start a project using the CLI (or Suite, for that matter), authorization already works. You can find **OAuth configuration** in the _environment.ts_ files. + +```js +import { Config } from '@abp/ng.core'; + +const baseUrl = 'http://localhost:4200'; + +export const environment = { + // other options removed for sake of brevity + + oAuthConfig: { + issuer: 'https://localhost:44305', + redirectUri: baseUrl, + clientId: 'MyProjectName_App', + responseType: 'code', + scope: 'offline_access MyProjectName', + }, + + // other options removed for sake of brevity +} as Config.Environment; + +``` + +This configuration results in an [OAuth authorization code flow with PKCE](https://tools.ietf.org/html/rfc7636) and we are using [angular-oauth2-oidc library](https://github.com/manfredsteyer/angular-oauth2-oidc#logging-in) for managing OAuth in the Angular client. + +According to this flow, the user is redirected to an external login page which is built with MVC. So, if you need **to customize the login page**, please follow [this community article](https://community.abp.io/articles/how-to-customize-the-login-page-for-mvc-razor-page-applications-9a40f3cd). diff --git a/docs/en/UI/Angular/Component-Replacement.md b/docs/en/UI/Angular/Component-Replacement.md index 577129e139..54a1869c0e 100644 --- a/docs/en/UI/Angular/Component-Replacement.md +++ b/docs/en/UI/Angular/Component-Replacement.md @@ -45,7 +45,7 @@ Run the following command to generate a layout in `angular` folder: yarn ng generate component my-application-layout ``` -Add the following code in your layout template (`my-layout.component.html`) where you want the page to be loaded. +Add the following code in your layout template (`my-application-layout.component.html`) where you want the page to be loaded. ```html @@ -350,7 +350,7 @@ import { ApplicationConfiguration, AuthService, ConfigState, - SessionState, + SessionStateService, SetLanguage, } from '@abp/ng.core'; import { Component, AfterViewInit } from '@angular/core'; @@ -396,10 +396,10 @@ export class NavItemsComponent implements AfterViewInit { } get selectedLangCulture(): string { - return this.store.selectSnapshot(SessionState.getLanguage); + return this.sessionState.getLanguage(); } - constructor(private store: Store, private authService: AuthService) {} + constructor(private store: Store, private authService: AuthService, private sessionState: SessionStateService) {} ngAfterViewInit() { fromEvent(window, 'resize') @@ -547,7 +547,3 @@ The final UI looks like below: ## See Also - [How to Replace PermissionManagementComponent](./Permission-Management-Component-Replacement.md) - -## What's Next? - -- [Custom Setting Page](./Custom-Setting-Page.md) diff --git a/docs/en/UI/Angular/Config-State-Service.md b/docs/en/UI/Angular/Config-State-Service.md new file mode 100644 index 0000000000..c810562623 --- /dev/null +++ b/docs/en/UI/Angular/Config-State-Service.md @@ -0,0 +1,135 @@ +# Config State Service + +`ConfigStateService` is a singleton service, i.e. provided in root level of your application, and keeps the application configuration response in the internal store. + +## Before Use + +In order to use the `ConfigStateService` you must inject it in your class as a dependency. + +```js +import { ConfigStateService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private config: ConfigStateService) {} +} +``` + +You do not have to provide the `ConfigStateService` at module or component/directive level, because it is already **provided in root**. + +## Get Methods + +`ConfigStateService` has numerous get methods which allow you to get a specific configuration or all configurations. + +Get methods with "$" at the end of the method name (e.g. `getAll$`) return an RxJs stream. The streams are triggered when set or patched the state. + +### How to Get All Configurations + +You can use the `getAll` or `getAll$` method of `ConfigStateService` to get all of the applcation configuration response object. It is used as follows: + +```js +// this.config is instance of ConfigStateService + +const config = this.config.getAll(); + +// or +this.config.getAll$().subscribe(config => { + // use config here +}) +``` + +### How to Get a Specific Configuration + +You can use the `getOne` or `getOne$` method of `ConfigStateService` to get a specific configuration property. For that, the property name should be passed to the method as parameter. + +```js +// this.config is instance of ConfigStateService + +const currentUser = this.config.getOne("currentUser"); + +// or +this.config.getOne$("currentUser").subscribe(currentUser => { + // use currentUser here +}) +``` + +On occasion, you will probably want to be more specific than getting just the current user. For example, here is how you can get the `tenantId`: + +```js +const tenantId = this.config.getDeep("currentUser.tenantId"); + +// or +this.config.getDeep$("currentUser.tenantId").subscribe(tenantId => { + // use tenantId here +}) +``` + +or by giving an array of keys as parameter: + +```js +const tenantId = this.config.getDeep(["currentUser", "tenantId"]); +``` + +FYI, `getDeep` is able to do everything `getOne` does. Just keep in mind that `getOne` is slightly faster. + +### How to Get a Feature + +You can use the `getFeature` or `getFeature$` method of `ConfigStateService` to get a feature value. For that, the feature name should be passed to the method as parameter. + +```js +// this.config is instance of ConfigStateService + +const enableLdapLogin = this.configStateService.getFeature("Account.EnableLdapLogin"); + +// or +this.config.getFeature$("Account.EnableLdapLogin").subscribe(enableLdapLogin => { + // use enableLdapLogin here +}) +``` + +> For more information, see the [features document](./Features). + +### How to Get a Setting + +You can use the `getSetting` or `getSetting$` method of `ConfigStateService` to get a setting. For that, the setting name should be passed to the method as parameter. + +```js +// this.config is instance of ConfigStateService + +const twoFactorBehaviour = this.configStateService.getSetting("Abp.Identity.TwoFactor.Behaviour"); + +// or +this.config.getSetting$("Abp.Identity.TwoFactor.Behaviour").subscribe(twoFactorBehaviour => { + // use twoFactorBehaviour here +}) +``` + +> For more information, see the [settings document](./Settings). + +#### State Properties + +Please refer to `ApplicationConfiguration.Response` type for all the properties you can get with `getOne` and `getDeep`. It can be found in the [application-configuration.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/application-configuration.ts#L4). + + +## Set State + +`ConfigStateService` has a method named `setState` which allow you to set the state value. + +You can get the application configuration response and set the `ConfigStateService` state value as shown below: + +```js +import {ApplicationConfigurationService, ConfigStateService} from '@abp/ng.core'; + +constructor(private applicationConfigurationService: ApplicationConfigurationService, private config: ConfigStateService) { + this.applicationConfigurationService.getConfiguration().subscribe(config => { + this.config.setState(config); + }) +} +``` + +## See Also + +- [Settings](./Settings.md) +- [Features](./Features.md) diff --git a/docs/en/UI/Angular/Config-State.md b/docs/en/UI/Angular/Config-State.md index b1a882d43a..d6774bb0cb 100644 --- a/docs/en/UI/Angular/Config-State.md +++ b/docs/en/UI/Angular/Config-State.md @@ -1,196 +1 @@ -# Config State - -`ConfigStateService` is a singleton service, i.e. provided in root level of your application, and is actually a façade for interacting with application configuration state in the `Store`. - -## Before Use - -In order to use the `ConfigStateService` you must inject it in your class as a dependency. - -```js -import { ConfigStateService } from '@abp/ng.core'; - -@Component({ - /* class metadata here */ -}) -class DemoComponent { - constructor(private config: ConfigStateService) {} -} -``` - -You do not have to provide the `ConfigStateService` at module or component/directive level, because it is already **provided in root**. - -## Selector Methods - -`ConfigStateService` has numerous selector methods which allow you to get a specific configuration or all configurations from the `Store`. - -### How to Get All Configurations From the Store - -You can use the `getAll` method of `ConfigStateService` to get all of the configuration object from the store. It is used as follows: - -```js -// this.config is instance of ConfigStateService - -const config = this.config.getAll(); -``` - -### How to Get a Specific Configuration From the Store - -You can use the `getOne` method of `ConfigStateService` to get a specific configuration property from the store. For that, the property name should be passed to the method as parameter. - -```js -// this.config is instance of ConfigStateService - -const currentUser = this.config.getOne("currentUser"); -``` - -On occasion, you will probably want to be more specific than getting just the current user. For example, here is how you can get the `tenantId`: - -```js -const tenantId = this.config.getDeep("currentUser.tenantId"); -``` - -or by giving an array of keys as parameter: - -```js -const tenantId = this.config.getDeep(["currentUser", "tenantId"]); -``` - -FYI, `getDeep` is able to do everything `getOne` does. Just keep in mind that `getOne` is slightly faster. - -#### Config State Properties - -Please refer to `Config.State` type for all the properties you can get with `getOne` and `getDeep`. It can be found in the [config.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L7). - -### How to Get the Application Information From the Store - -The `getApplicationInfo` method is used to get the application information from the environment variables stored as the config state. This is how you can use it: - -```js -// this.config is instance of ConfigStateService - -const appInfo = this.config.getApplicationInfo(); -``` - -This method never returns `undefined` or `null` and returns an empty object literal (`{}`) instead. In other words, you will never get an error when referring to the properties of `appInfo` above. - -#### Application Information Properties - -Please refer to `Config.Application` type for all the properties you can get with `getApplicationInfo`. It can be found in the [config.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L21). - -### How to Get API URL From the Store - -The `getApplicationInfo` method is used to get a specific API URL from the environment variables stored as the config state. This is how you can use it: - -```js -// this.config is instance of ConfigStateService - -const apiUrl = this.config.getApiUrl(); -// environment.apis.default.url - -const searchUrl = this.config.getApiUrl("search"); -// environment.apis.search.url -``` - -This method returns the `url` of a specific API based on the key given as its only parameter. If there is no key, `'default'` is used. - -### How to Get a Specific Permission From the Store - -You can use the `getGrantedPolicy` method of `ConfigStateService` to get a specific permission from the configuration state. For that, you should pass a policy key as parameter to the method. - -```js -// this.config is instance of ConfigStateService - -const hasIdentityPermission = this.config.getGrantedPolicy("Abp.Identity"); -// true -``` - -You may also **combine policy keys** to fine tune your selection: - -```js -// this.config is instance of ConfigStateService - -const hasIdentityAndAccountPermission = this.config.getGrantedPolicy( - "Abp.Identity && Abp.Account" -); -// false - -const hasIdentityOrAccountPermission = this.config.getGrantedPolicy( - "Abp.Identity || Abp.Account" -); -// true -``` - -Please consider the following **rules** when creating your permission selectors: - -- Maximum 2 keys can be combined. -- `&&` operator looks for both keys. -- `||` operator looks for either key. -- Empty string `''` as key will return `true` -- Using an operator without a second key will return `false` - -### How to Get Translations From the Store - -The `getLocalization` method of `ConfigStateService` is used for translations. Here are some examples: - -```js -// this.config is instance of ConfigStateService - -const identity = this.config.getLocalization("AbpIdentity::Identity"); -// 'identity' - -const notFound = this.config.getLocalization("AbpIdentity::IDENTITY"); -// 'AbpIdentity::IDENTITY' - -const defaultValue = this.config.getLocalization({ - key: "AbpIdentity::IDENTITY", - defaultValue: "IDENTITY" -}); -// 'IDENTITY' -``` - -Please check out the [localization documentation](./Localization.md) for details. - -## Dispatch Methods - -`ConfigStateService` has several dispatch methods which allow you to conveniently dispatch predefined actions to the `Store`. - -### How to Get Application Configuration From Server - -The `dispatchGetAppConfiguration` triggers a request to an endpoint that responds with the application state and then places this response to the `Store` as configuration state. - -```js -// this.config is instance of ConfigStateService - -this.config.dispatchGetAppConfiguration(); -// returns a state stream which emits after dispatch action is complete -``` - -Note that **you do not have to call this method at application initiation**, because the application configuration is already being received from the server at start. - -### How to Set the Environment - -The `dispatchSetEnvironment` places environment variables passed to it in the `Store` under the configuration state. Here is how it is used: - -```js -// this.config is instance of ConfigStateService - -this.config.dispatchSetEnvironment({ - /* environment properties here */ -}); -// returns a state stream which emits after dispatch action is complete -``` - -Note that **you do not have to call this method at application initiation**, because the environment variables are already being stored at start. - -#### Environment Properties - -Please refer to `Config.Environment` type for all the properties you can pass to `dispatchSetEnvironment` as parameter. It can be found in the [config.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L13). - -## See Also - -- [Settings](./Settings.md) -- [Features](./Features.md) - -## What's Next? - -- [HTTP Requests](./Http-Requests) +**ConfigState has been deprecated.** Use the [ConfigStateService](./Config-State-Service) instead. \ No newline at end of file diff --git a/docs/en/UI/Angular/Confirmation-Service.md b/docs/en/UI/Angular/Confirmation-Service.md index 16dfa9fd94..ef48332580 100644 --- a/docs/en/UI/Angular/Confirmation-Service.md +++ b/docs/en/UI/Angular/Confirmation-Service.md @@ -180,8 +180,3 @@ clear( ``` - `status` parameter is the value of the confirmation closing event. - - -## What's Next? - -- [Toast Overlay](./Toaster-Service.md) diff --git a/docs/en/UI/Angular/Content-Projection-Service.md b/docs/en/UI/Angular/Content-Projection-Service.md index 7e03776835..ee64f57e20 100644 --- a/docs/en/UI/Angular/Content-Projection-Service.md +++ b/docs/en/UI/Angular/Content-Projection-Service.md @@ -71,8 +71,3 @@ projectContent | TemplateRef>( - `projectionStrategy` parameter is the primary focus here and is explained above. - `injector` parameter is the `Injector` instance you can pass to the projected content. It is not used in `TemplateProjectionStrategy`. - - -## What's Next? - -- [Confirmation Popup](./Confirmation-Service.md) diff --git a/docs/en/UI/Angular/Cross-Origin-Strategy.md b/docs/en/UI/Angular/Cross-Origin-Strategy.md index e626bf5011..5edcbed9e6 100644 --- a/docs/en/UI/Angular/Cross-Origin-Strategy.md +++ b/docs/en/UI/Angular/Cross-Origin-Strategy.md @@ -51,10 +51,3 @@ CROSS_ORIGIN_STRATEGY.UseCredentials(integrity?: string) ``` `crossorigin` will be set as `"use-credentials"` and `integrity` is optional. - - - - -## What's Next? - -- [LoadingStrategy](./Loading-Strategy.md) diff --git a/docs/en/UI/Angular/Data-Table-Column-Extensions.md b/docs/en/UI/Angular/Data-Table-Column-Extensions.md new file mode 100644 index 0000000000..6f1367d934 --- /dev/null +++ b/docs/en/UI/Angular/Data-Table-Column-Extensions.md @@ -0,0 +1,320 @@ +# Data Table Column (or Entity Prop) Extensions for Angular UI + + +## Introduction + +Entity prop extension system allows you to add a new column to the data table for an entity or change/remove an already existing one. A "Name" column was added to the user management page below: + +Entity Prop Extension Example: 'Name' Column + +You will have access to the current entity in your code and display its value, make the column sortable, perform visibility checks, and more. You can also render custom HTML in table cells. + +## How to Set Up + +In this example, we will add a "Name" column and display the value of the `name` field in the user management page of the [Identity Module](../../Modules/Identity.md). + +### Step 1. Create Entity Prop Contributors + +The following code prepares a constant named `identityEntityPropContributors`, ready to be imported and used in your root module: + +```js +// src/app/entity-prop-contributors.ts + +import { + eIdentityComponents, + IdentityEntityPropContributors, + IdentityUserDto, +} from '@abp/ng.identity'; +import { EntityProp, EntityPropList, ePropType } from '@abp/ng.theme.shared/extensions'; + +const nameProp = new EntityProp({ + type: ePropType.String, + name: 'name', + displayName: 'AbpIdentity::Name', + sortable: true, + columnWidth: 250, +}); + +export function namePropContributor(propList: EntityPropList) { + propList.addAfter(nameProp, 'userName', (value, name) => value.name === name); +} + +export const identityEntityPropContributors: IdentityEntityPropContributors = { + // enum indicates the page to add contributors to + [eIdentityComponents.Users]: [ + namePropContributor, + // You can add more contributors here + ], +}; + +``` + +The list of props, conveniently named as `propList`, is a **doubly linked list**. That is why we have used the `addAfter` method, which adds a node with given value after the first node that has the previous value. You may find [all available methods here](../Common/Utils/Linked-List.md). + +### Step 2. Import and Use Entity Prop Contributors + +Import `identityEntityPropContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below: + +```js +// src/app/app-routing.module.ts + +// other imports +import { identityEntityPropContributors } from './entity-prop-contributors'; + +const routes: Routes = [ + // other routes + + { + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + entityPropContributors: identityEntityPropContributors, + }) + ), + }, + + // other routes +]; +``` + +That is it, `nameProp` entity prop will be added, and you will see the "Name" column next to the usernames on the grid in the users page (`UsersComponent`) of the `IdentityModule`. + +## How to Render Custom HTML in Cells + +You can use the `valueResolver` to render an HTML string in the table. Imagine we want to show a red times icon (❌) next to unconfirmed emails and phones, instead of showing a green check icon next to confirmed emails and phones. The contributors below would do that for you. + +Entity Prop Extension Example: Custom Cell Render + +```js +// src/app/entity-prop-contributors.ts + +import { + eIdentityComponents, + IdentityEntityPropContributors, + IdentityUserDto, +} from '@abp/ng.identity'; +import { EntityProp, EntityPropList } from '@abp/ng.theme.shared/extensions'; +import { of } from 'rxjs'; + +export function emailPropContributor(propList: EntityPropList) { + const index = propList.indexOf('email', (value, name) => value.name === name); + const droppedNode = propList.dropByIndex(index); + const emailProp = new EntityProp({ + ...droppedNode.value, + valueResolver: data => { + const { email, emailConfirmed } = data.record; + const icon = email && !emailConfirmed ? `` : ''; + + return of((email || '') + icon); // should return an observable + }, + }); + + propList.addByIndex(emailProp, index); +} + +export function phonePropContributor(propList: EntityPropList) { + const index = propList.indexOf('phoneNumber', (value, name) => value.name === name); + const droppedNode = propList.dropByIndex(index); + const phoneProp = new EntityProp({ + ...droppedNode.value, + valueResolver: data => { + const { phoneNumber, phoneNumberConfirmed } = data.record; + const icon = + phoneNumber && !phoneNumberConfirmed ? `` : ''; + + return of((phoneNumber || '') + icon); // should return an observable + }, + }); + + propList.addByIndex(phoneProp, index); +} + +export const identityEntityPropContributors: IdentityEntityPropContributors = { + [eIdentityComponents.Users]: [emailPropContributor, phonePropContributor], +}; + +``` + +> The `valueResolver` method should return an observable. You can wrap your return values with `of` from RxJS for that. + +## Object Extensions + +Extra properties defined on an existing entity will be included in the table based on their configuration. The values will also be mapped to and from `extraProperties` automatically. They are available when defining custom contributors, so you can drop, modify, or reorder them. The `isExtra` identifier will be set to `true` for these properties and will define this automatic behavior. + +## API + +### PropData\ + +`PropData` is the shape of the parameter passed to all callbacks or predicates in an `EntityProp`. + +It has the following properties: + +- **record** is the row data, i.e. current value rendered in the table. + + ```js + { + type: ePropType.String, + name: 'name', + valueResolver: data => { + const name = data.record.name || ''; + return of(name.toUpperCase()); + }, + } + ``` + +- **index** is the table index where the record is at. + +- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `ExtensibleTableComponent`, including, but not limited to, its parent component. + + ```js + { + type: ePropType.String, + name: 'name', + valueResolver: data => { + const restService = data.getInjected(RestService); + const usersComponent = data.getInjected(UsersComponent); + + // Use restService and usersComponent public props and methods here + }, + } + ``` + +### PropCallback\ + +`PropCallback` is the type of the callback function that can be passed to an `EntityProp` as `prop` parameter. A prop callback gets a single parameter, the `PropData`. The return type may be anything, including `void`. Here is a simplified representation: + +```js +type PropCallback = (data?: PropData) => R; +``` + +### PropPredicate\ + +`PropPredicate` is the type of the predicate function that can be passed to an `EntityProp` as `visible` parameter. A prop predicate gets a single parameter, the `PropData`. The return type must be `boolean`. Here is a simplified representation: + +```js +type PropPredicate = (data?: PropData) => boolean; +``` + +### EntityPropOptions\ + +`EntityPropOptions` is the type that defines required and optional properties you have to pass in order to create an entity prop. + +Its type definition is as follows: + +```js +type EntityPropOptions = { + type: ePropType; + name: string; + displayName?: string; + valueResolver?: PropCallback>; + sortable?: boolean; + columnWidth?: number; + permission?: string; + visible?: PropPredicate; +}; +``` + +As you see, passing `type` and `name` is enough to create an entity prop. Here is what each property is good for: + +- **type** is the type of the prop value. It is used for custom rendering in the table. (_required_) +- **name** is the property name (or key) which will be used to read the value of the prop. (_required_) +- **displayName** is the name of the property which will be localized and shown as column header. (_default:_ `options.name`) +- **valueResolver** is a callback that is called when the cell is rendered. It must return an observable. (_default:_ `data => of(data.record[options.name])`) +- **sortable** defines if the table is sortable based on this entity prop. Sort icons are shown based on it. (_default:_ `false`) +- **columnWidth** defines a minimum width for the column. Good for horizontal scroll. (_default:_ `undefined`) +- **permission** is the permission context which will be used to decide if a column for this entity prop should be displayed to the user or not. (_default:_ `undefined`) +- **visible** is a predicate that will be used to decide if this entity prop should be displayed on the table or not. (_default:_ `() => true`) + +> Important Note: Do not use record in visibility predicates. First of all, the table header checks it too and the record will be `undefined`. Second, if some cells are displayed and others are not, the table will be broken. Use the `valueResolver` and render an empty cell when you need to hide a specific cell. + +You may find a full example below. + +### EntityProp\ + +`EntityProp` is the class that defines your entity props. It takes an `EntityPropOptions` and sets the default values to the properties, creating an entity prop that can be passed to an entity contributor. + +```js +const options: EntityPropOptions = { + type: ePropType.String, + name: 'email', + displayName: 'AbpIdentity::EmailAddress', + valueResolver: data => { + const { email, emailConfirmed } = data.record; + + return of( + (email || '') + (emailConfirmed ? `` : ''), + ); + }, + sortable: true, + columnWidth: 250, + permission: 'AbpIdentity.Users.ReadSensitiveData', // hypothetical + visible: data => { + const store = data.getInjected(Store); + const selectSensitiveDataVisibility = ConfigState.getSetting( + 'Abp.Identity.IsSensitiveDataVisible' // hypothetical + ); + + return store.selectSnapshot(selectSensitiveDataVisibility).toLowerCase() === 'true'; + } +}; + +const prop = new EntityProp(options); +``` + +It also has two static methods to create its instances: + +- **EntityProp.create\\(options: EntityPropOptions\\)** is used to create an instance of `EntityProp`. + ```js + const prop = EntityProp.create(options); + ``` +- **EntityProp.createMany\\(options: EntityPropOptions\\[\]\)** is used to create multiple instances of `EntityProp` with given array of `EntityPropOptions`. + ```js + const props = EntityProp.createMany(optionsArray); + ``` + +### EntityPropList\ + +`EntityPropList` is the list of props passed to every prop contributor callback as the first parameter named `propList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md). + +The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this: + +```js +export function reorderUserContributors( + propList: EntityPropList, +) { + // drop email node + const emailPropNode = propList.dropByValue( + 'AbpIdentity::EmailAddress', + (prop, text) => prop.text === text, + ); + + // add it back after phoneNumber + propList.addAfter( + emailPropNode.value, + 'phoneNumber', + (value, name) => value.name === name, + ); +} +``` + +### EntityPropContributorCallback\ + +`EntityPropContributorCallback` is the type that you can pass as entity prop contributor callbacks to static `forLazy` methods of the modules. + +```js +export function isLockedOutPropContributor( + propList: EntityPropList, +) { + // add isLockedOutProp as 2nd column + propList.add(isLockedOutProp).byIndex(1); +} + +export const identityEntityPropContributors = { + [eIdentityComponents.Users]: [isLockedOutPropContributor], +}; +``` + +## See Also + +- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md) diff --git a/docs/en/UI/Angular/Dom-Insertion-Service.md b/docs/en/UI/Angular/Dom-Insertion-Service.md index d56e4f8b0e..78e01c59fb 100644 --- a/docs/en/UI/Angular/Dom-Insertion-Service.md +++ b/docs/en/UI/Angular/Dom-Insertion-Service.md @@ -134,7 +134,3 @@ has(content: string): boolean The `has` method returns a boolean value that indicates the given content has already been added to the DOM or not. - `content` parameter is the content of the inserted `HTMLScriptElement` or `HTMLStyleElement` element. - -## What's Next? - -- [Lazy Loading Scripts & Styles](./Lazy-Load-Service.md) diff --git a/docs/en/UI/Angular/Dynamic-Form-Extensions.md b/docs/en/UI/Angular/Dynamic-Form-Extensions.md new file mode 100644 index 0000000000..d7780ceec0 --- /dev/null +++ b/docs/en/UI/Angular/Dynamic-Form-Extensions.md @@ -0,0 +1,315 @@ +# Dynamic Form (or Form Prop) Extensions for Angular UI + + +## Introduction + +Form prop extension system allows you to add a new field to the create and/or edit forms for a form or change/remove an already existing one. A "Date of Birth" field was added to the user management page below: + +Form Prop Extension Example: 'Date of Birth' Field + +You can validate the field, perform visibility checks, and do more. You will also have access to the current entity when creating a contibutor for an edit form. + +## How to Set Up + +In this example, we will add a "Date of Birth" field in the user management page of the [Identity Module](../../Modules/Identity.md) and validate it. + +### Step 1. Create Form Prop Contributors + +The following code prepares two constants named `identityCreateFormPropContributors` and `identityEditFormPropContributors`, ready to be imported and used in your root module: + +```js +// src/app/form-prop-contributors.ts + +import { + eIdentityComponents, + IdentityCreateFormPropContributors, + IdentityUserDto, +} from '@abp/ng.identity'; +import { ePropType, FormProp, FormPropList } from '@abp/ng.theme.shared/extensions'; +import { Validators } from '@angular/forms'; + +const birthdayProp = new FormProp({ + type: ePropType.Date, + name: 'birthday', + displayName: 'AbpIdentity::Birthday', + validators: () => [Validators.required], +}); + +export function birthdayPropContributor(propList: FormPropList) { + propList.addByIndex(birthdayProp, 4); +} + +export const identityCreateFormPropContributors: IdentityCreateFormPropContributors = { + // enum indicates the page to add contributors to + [eIdentityComponents.Users]: [ + birthdayPropContributor, + // You can add more contributors here + ], +}; + +export const identityEditFormPropContributors = identityCreateFormPropContributors; +// you may define different contributors for edit form if you like + +``` + + +The list of props, conveniently named as `propList`, is a **doubly linked list**. That is why we have used the `addByIndex` method, which adds the given value to the specified index of the list. You may find [all available methods here](../Common/Utils/Linked-List.md). + +### Step 2. Import and Use Form Prop Contributors + +Import `identityCreateFormPropContributors` and `identityEditFormPropContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below: + +```js +// src/app/app-routing.module.ts + +// other imports +import { + identityCreateFormPropContributors, + identityEditFormPropContributors, +} from './form-prop-contributors'; + +const routes: Routes = [ + // other routes + + { + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + createFormPropContributors: identityCreateFormPropContributors, + editFormPropContributors: identityEditFormPropContributors, + }) + ), + }, + + // other routes +]; +``` + +That is it, `birthdayProp` form prop will be added, and you will see the datepicker for the "Date of Birth" field right before the "Email address" in the forms of the users page in the `IdentityModule`. + +## Object Extensions + +Extra properties defined on an existing entity will be included in the create and edit forms and validated based on their configuration. The form values will also be mapped to and from `extraProperties` automatically. They are available when defining custom contributors, so you can drop, modify, or reorder them. The `isExtra` identifier will be set to `true` for these properties and will define this automatic behavior. + +## API + +### PropData\ + +`PropData` is the shape of the parameter passed to all callbacks or predicates in a `FormProp`. + +It has the following properties: + +- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `ExtensibleFormPropComponent`, including, but not limited to, its parent components. + + ```js + { + type: ePropType.Enum, + name: 'myField', + options: data => { + const restService = data.getInjected(RestService); + const usersComponent = data.getInjected(UsersComponent); + + // Use restService and usersComponent public props and methods here + } + }, + ``` + +- **record** is the row data, i.e. current value of the selected item to edit. This property is _available only on edit forms_. + + ```js + { + type: ePropType.String, + name: 'myProp', + readonly: data => data.record.someOtherProp, + } + ``` + +### PropCallback\ + +`PropCallback` is the type of the callback function that can be passed to a `FormProp` as `prop` parameter. A prop callback gets a single parameter, the `PropData`. The return type may be anything, including `void`. Here is a simplified representation: + +```js +type PropCallback = (data?: PropData) => R; +``` + +### PropPredicate\ + +`PropPredicate` is the type of the predicate function that can be passed to a `FormProp` as `visible` parameter. A prop predicate gets a single parameter, the `PropData`. The return type must be `boolean`. Here is a simplified representation: + +```js +type PropPredicate = (data?: PropData) => boolean; +``` + +### FormPropOptions\ + +`FormPropOptions` is the type that defines required and optional properties you have to pass in order to create a form prop. + +Its type definition is as follows: + +```js +type FormPropOptions = { + type: ePropType; + name: string; + displayName?: string; + id?: string; + permission?: string; + visible?: PropPredicate; + readonly?: PropPredicate; + disabled?: PropPredicate; + validators?: PropCallback; + asyncValidators?: PropCallback; + defaultValue?: boolean | number | string | Date; + options?: PropCallback[]>>; + autocomplete?: string; + isExtra? boolean; +}; +``` + +As you see, passing `type` and `name` is enough to create a form prop. Here is what each property is good for: + +- **type** is the type of the prop value. It defines which input is rendered for the prop in the form. (_required_) +- **name** is the property name (or key) which will be used to read the value of the prop. (_required_) +- **displayName** is the name of the property which will be localized and shown as column header. (_default:_ `options.name`) +- **id** will be set as the `for` attribute of the label and the `id` attribute of the input for the field. (_default:_ `options.name`) +- **permission** is the permission context which will be used to decide if a column for this form prop should be displayed to the user or not. (_default:_ `undefined`) +- **visible** is a predicate that will be used to decide if this prop should be displayed on the form or not. (_default:_ `() => true`) +- **readonly** is a predicate that will be used to decide if this prop should be readonly or not. (_default:_ `() => false`) +- **disabled** is a predicate that will be used to decide if this prop should be disabled or not. (_default:_ `() => false`) +- **validators** is a callback that returns validators for the prop. (_default:_ `() => []`) +- **asyncValidators** is a callback that returns async validators for the prop. (_default:_ `() => []`) +- **defaultValue** is the initial value the field will have. (_default:_ `null`) +- **options** is a callback that is called when a dropdown is needed. It must return an observable. (_default:_ `undefined`) +- **autocomplete** will be set as the `autocomplete` attribute of the input for the field. Please check [possible values](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#Values). (_default:_ `'off'`) +- **isExtra** indicates this prop is an object extension. When `true`, the value of the field will be mapped from and to `extraProperties` of the entity. (_default:_ `undefined`) + +> Important Note: Do not use `record` property of `PropData` in create form predicates and callbacks, because it will be `undefined`. You can use it on edit form contributors though. + +You may find a full example below. + +### FormProp\ + +`FormProp` is the class that defines your form props. It takes a `FormPropOptions` and sets the default values to the properties, creating a form prop that can be passed to a form contributor. + +```js +const options: FormPropOptions = { + type: ePropType.Enum, + name: 'myProp', + displayName: 'Default::MyPropName', + id: 'my-prop', + permission: 'AbpIdentity.Users.ReadSensitiveData', // hypothetical + visible: data => { + const store = data.getInjected(Store); + const selectSensitiveDataVisibility = ConfigState.getSetting( + 'Abp.Identity.IsSensitiveDataVisible' // hypothetical + ); + + return store.selectSnapshot(selectSensitiveDataVisibility).toLowerCase() === 'true'; + }, + readonly: data => data.record.someProp, + disabled: data => data.record.someOtherProp, + validators: () => [Validators.required], + asyncValidators: data => { + const http = data.getInjected(HttpClient); + + function validate(control: AbstractControl): Observable { + if (control.pristine) return of(null); + + return http + .get('https://api.my-brand.io/hypothetical/endpoint/' + control.value) + .pipe(map(response => (response.valid ? null : { invalid: true }))); + } + + return [validate]; + }, + defaultValue: 0, + options: data => { + const service = data.getInjected(MyIdentityService); + + return service.getMyPropOptions() + .pipe( + map(({items}) => items.map( + item => ({key: item.name, value: item.id }) + )), + ); + }, + autocomplete: 'off', + isExtra: true, +}; + +const prop = new FormProp(options); +``` + +It also has two static methods to create its instances: + +- **FormProp.create\\(options: FormPropOptions\\)** is used to create an instance of `FormProp`. + ```js + const prop = FormProp.create(options); + ``` +- **FormProp.createMany\\(options: FormPropOptions\\[\]\)** is used to create multiple instances of `FormProp` with given array of `FormPropOptions`. + ```js + const props = FormProp.createMany(optionsArray); + ``` + +### FormPropList\ + +`FormPropList` is the list of props passed to every prop contributor callback as the first parameter named `propList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md). + +The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this: + +```js +export function reorderUserContributors( + propList: FormPropList, +) { + // drop email node + const emailPropNode = propList.dropByValue( + 'AbpIdentity::EmailAddress', + (prop, displayName) => prop.displayName === displayName, + ); + + // add it back after phoneNumber + propList.addAfter( + emailPropNode.value, + 'phoneNumber', + (value, name) => value.name === name, + ); +} +``` + +### CreateFormPropContributorCallback\ + +`CreateFormPropContributorCallback` is the type that you can pass as **create form** prop contributor callbacks to static `forLazy` methods of the modules. + +```js +export function myPropCreateContributor( + propList: FormPropList, +) { + // add myProp as 2nd field from the start + propList.add(myProp).byIndex(1); +} + +export const identityCreateFormPropContributors = { + [eIdentityComponents.Users]: [myPropCreateContributor], +}; +``` + +### EditFormPropContributorCallback\ + +`EditFormPropContributorCallback` is the type that you can pass as **edit form** prop contributor callbacks to static `forLazy` methods of the modules. + +```js +export function myPropEditContributor( + propList: FormPropList, +) { + // add myProp as 2nd field from the end + propList.add(myProp).byIndex(-1); +} + +export const identityEditFormPropContributors = { + [eIdentityComponents.Users]: [myPropEditContributor], +}; +``` + +## See Also + +- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md) diff --git a/docs/en/UI/Angular/Entity-Action-Extensions.md b/docs/en/UI/Angular/Entity-Action-Extensions.md new file mode 100644 index 0000000000..fd22e293fd --- /dev/null +++ b/docs/en/UI/Angular/Entity-Action-Extensions.md @@ -0,0 +1,400 @@ +# Entity Action Extensions for Angular UI + +## Introduction + +Entity action extension system allows you to add a new action to the action menu for an entity. A "Click Me" action was added to the user management page below: + +Entity Action Extension Example: 'Click Me!' Action + +You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can also access the current entity in your code. + +## How to Set Up + +In this example, we will add a "Click Me!" action and alert the current row's `userName` in the user management page of the [Identity Module](../../Modules/Identity.md). + +### Step 1. Create Entity Action Contributors + +The following code prepares a constant named `identityEntityActionContributors`, ready to be imported and used in your root module: + +```js +// src/app/entity-action-contributors.ts + +import { + eIdentityComponents, + IdentityEntityActionContributors, + IdentityUserDto, +} from '@abp/ng.identity'; +import { EntityAction, EntityActionList } from '@abp/ng.theme.shared/extensions'; + +const alertUserName = new EntityAction({ + text: 'Click Me!', + action: data => { + // Replace alert with your custom code + alert(data.record.userName); + }, + // See EntityActionOptions in API section for all options +}); + +export function alertUserNameContributor(actionList: EntityActionList) { + actionList.addTail(alertUserName); +} + +export const identityEntityActionContributors: IdentityEntityActionContributors = { + // enum indicates the page to add contributors to + [eIdentityComponents.Users]: [ + alertUserNameContributor, + // You can add more contributors here + ], +}; +``` + +The list of actions, conveniently named as `actionList`, is a **doubly linked list**. That is why we have used the `addTail` method, which adds the given value to the end of the list. You may find [all available methods here](../Common/Utils/Linked-List.md). + +### Step 2. Import and Use Entity Action Contributors + +Import `identityEntityActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below: + +```js +// src/app/app-routing.module.ts + +// other imports +import { identityEntityActionContributors } from './entity-action-contributors'; + +const routes: Routes = [ + // other routes + + { + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + entityActionContributors: identityEntityActionContributors, + }) + ), + }, + + // other routes +]; +``` + +That is it, `alertUserName` entity action will be added as the last action on the grid dropdown in the "Users" page (`UsersComponent`) of the `IdentityModule`. + +## How to Place a Custom Modal and Trigger It by Entity Actions + +Let's employ dependency injection to extend the functionality of `IdentityModule` and add a quick view action for the User entity. We will take a lazy-loaded approach. + +Entity Action Extension Example: Custom Modal + +1. Create a folder at this path: `src/app/identity-extended` + +2. Add an entity action similar to this: + ```js + // src/app/identity-extended/entity-action-contributors.ts + + import { + eIdentityComponents, + IdentityEntityActionContributors, + IdentityUserDto, + } from '@abp/ng.identity'; + import { EntityAction, EntityActionList } from '@abp/ng.theme.shared/extensions'; + import { IdentityExtendedComponent } from './identity-extended.component'; + + const quickViewAction = new EntityAction({ + text: 'Quick View', + action: data => { + const component = data.getInjected(IdentityExtendedComponent); + component.openUserQuickView(data.record); + }, + }); + + export function customModalContributor(actionList: EntityActionList) { + actionList.addTail(quickViewAction); + } + + export const identityEntityActionContributors: IdentityEntityActionContributors = { + // enum indicates the page to add contributors to + [eIdentityComponents.Users]: [ + customModalContributor, + // You can add more contributors here + ], + }; + ``` + +3. Create a parent component to the identity module. + ```js + // src/app/identity-extended/identity-extended.component.ts + + import { IdentityUserDto } from '@abp/ng.identity'; + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-identity-extended', + templateUrl: './identity-extended.component.html', + }) + export class IdentityExtendedComponent { + isUserQuickViewVisible: boolean; + + user: IdentityUserDto; + + openUserQuickView(record: IdentityUserDto) { + this.user = new Proxy(record, { + get: (target, prop) => target[prop] || '—', + }); + this.isUserQuickViewVisible = true; + } + } + ``` + +4. Add a router outlet and a modal to the parent component. + ```html + + + + + + +

{%{{{ user.userName }}}%}

+
+ + + + + + + + + + + + + + + + + + + + + +
{%{{{ 'AbpIdentity::DisplayName:Name' | abpLocalization }}}%}{%{{{ user.name }}}%}
{%{{{ 'AbpIdentity::DisplayName:Surname' | abpLocalization }}}%}{%{{{ user.surname }}}%}
{%{{{ 'AbpIdentity::EmailAddress' | abpLocalization }}}%}{%{{{ user.email }}}%}
{%{{{ 'AbpIdentity::PhoneNumber' | abpLocalization }}}%}{%{{{ user.phoneNumber }}}%}
+
+ + + + +
+ ``` + +5. Add a module for the component and load `IdentityModule` as seen below: + ```js + // src/app/identity-extended/identity-extended.module.ts + + import { CoreModule } from '@abp/ng.core'; + import { IdentityModule } from '@abp/ng.identity'; + import { ThemeSharedModule } from '@abp/ng.theme.shared'; + import { NgModule } from '@angular/core'; + import { RouterModule } from '@angular/router'; + import { identityEntityActionContributors } from './entity-action-contributors'; + import { IdentityExtendedComponent } from './identity-extended.component'; + + @NgModule({ + imports: [ + CoreModule, + ThemeSharedModule, + RouterModule.forChild([ + { + path: '', + component: IdentityExtendedComponent, + children: [ + { + path: '', + loadChildren: () => + IdentityModule.forLazy({ + entityActionContributors: identityEntityActionContributors, + }), + }, + ], + }, + ]), + ], + declarations: [IdentityExtendedComponent], + }) + export class IdentityExtendedModule {} + ``` + +6. Load `IdentityExtendedModule` instead of `IdentityModule` in your root routing module. + ```js + // src/app/app-routing.module.ts + + const routes: Routes = [ + // other routes + + { + path: 'identity', + loadChildren: () => + import('./identity-extended/identity-extended.module') + .then(m => m.IdentityExtendedModule), + }, + + // other routes + ]; + ``` + +That's it. As you see, we reached the `IdentityExtendedComponent` through dependency injection and called one of its methods in our action. The specific user was also available via `data.record`, so we were able to display a summary view. + +## API + +### ActionData\ + +`ActionData` is the shape of the parameter passed to all callbacks or predicates in an `EntityAction`. + +It has the following properties: + +- **record** is the row data, i.e. current value rendered in the table. + + ```js + { + text: 'Click Me!', + action: data => { + alert(data.record.userName); + }, + } + ``` + +- **index** is the table index where the record is at. + +- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `GridActionsComponent`, including, but not limited to, its parent component. + + ```js + { + text: 'Click Me!', + action: data => { + const restService = data.getInjected(RestService); + + // Use restService public props and methods here + }, + visible: data => { + const usersComponent = data.getInjected(UsersComponent); + + // Use usersComponent public props and methods here + }, + } + ``` + +### ActionCallback\ + +`ActionCallback` is the type of the callback function that can be passed to an `EntityAction` as `action` parameter. An action callback gets a single parameter, the `ActionData`. The return type may be anything, including `void`. Here is a simplified representation: + +```js +type ActionCallback = (data?: ActionData) => R; +``` + +### ActionPredicate\ + +`ActionPredicate` is the type of the predicate function that can be passed to an `EntityAction` as `visible` parameter. An action predicate gets a single parameter, the `ActionData`. The return type must be `boolean`. Here is a simplified representation: + +```js +type ActionPredicate = (data?: ActionData) => boolean; +``` + +### EntityActionOptions\ + +`EntityActionOptions` is the type that defines required and optional properties you have to pass in order to create an entity action. + +Its type definition is as follows: + +```js +type EntityActionOptions = { + action: ActionCallback, + text: string, + icon?: string, + permission?: string, + visible?: ActionPredicate, +}; +``` + +As you see, passing `action` and `text` is enough to create an entity action. Here is what each property is good for: + +- **action** is a callback that is called when the grid action is clicked. (_required_) +- **text** is the button text which will be localized. (_required_) +- **icon** is the classes that define an icon to be placed before the text. (_default:_ `''`) +- **permission** is the permission context which will be used to decide if this type of grid action should be displayed to the user or not. (_default:_ `undefined`) +- **visible** is a predicate that will be used to decide if the current record should have this grid action or not. (_default:_ `() => true`) + +You may find a full example below. + +### EntityAction\ + +`EntityAction` is the class that defines your entity actions. It takes an `EntityActionOptions` and sets the default values to the properties, creating an entity action that can be passed to an entity contributor. + +```js +const options: EntityActionOptions = { + action: data => { + const component = data.getInjected(IdentityExtendedComponent); + component.unlock(data.record.id); + }, + text: 'AbpIdentity::Unlock', + icon: 'fa fa-unlock', + permission: 'AbpIdentity.Users.Update', + visible: data => data.record.isLockedOut, +}; + +const action = new EntityAction(options); +``` + +It also has two static methods to create its instances: + +- **EntityAction.create\\(options: EntityActionOptions\\)** is used to create an instance of `EntityAction`. + ```js + const action = EntityAction.create(options); + ``` +- **EntityAction.createMany\\(options: EntityActionOptions\\[\]\)** is used to create multiple instances of `EntityAction` with given array of `EntityActionOptions`. + ```js + const actions = EntityAction.createMany(optionsArray); + ``` + +### EntityActionList\ + +`EntityActionList` is the list of actions passed to every action contributor callback as the first parameter named `actionList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md). + +The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this: + +```js +export function reorderUserContributors( + actionList: EntityActionList, +) { + // drop "Unlock" button + const unlockActionNode = actionList.dropByValue( + 'AbpIdentity::Unlock', + (action, text) => action.text === text, + ); + + // add it back to the head of the list + actionList.addHead(unlockActionNode.value); +} +``` + +### EntityActionContributorCallback\ + +`EntityActionContributorCallback` is the type that you can pass as entity action contributor callbacks to static `forLazy` methods of the modules. + +```js +// lockUserContributor should have EntityActionContributorCallback type + +export function lockUserContributor( + actionList: EntityActionList, +) { + // add lockUser as 3rd action + actionList.add(lockUser).byIndex(2); +} + +export const identityEntityActionContributors = { + [eIdentityComponents.Users]: [lockUserContributor], +}; +``` + +## See Also + +- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md) diff --git a/docs/en/UI/Angular/Environment.md b/docs/en/UI/Angular/Environment.md index 1b78a11cc6..8c6fb28ce7 100644 --- a/docs/en/UI/Angular/Environment.md +++ b/docs/en/UI/Angular/Environment.md @@ -102,7 +102,80 @@ export interface RemoteEnv { * `method`: HTTP method to be used when retrieving environment config. Default: `GET` * `headers`: If extra headers are needed for the request, it can be set through this field. +## EnvironmentService -## What's Next? +` EnvironmentService` is a singleton service, i.e. provided in root level of your application, and keeps the environment in the internal store. -- [About Feature Libraries](./Feature-Libraries.md) + +### Before Use + +In order to use the `EnvironmentService` you must inject it in your class as a dependency. + +```js +import { EnvironmentService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ +}) +class DemoComponent { + constructor(private environment: EnvironmentService) {} +} +``` + +You do not have to provide the `EnvironmentService` at module or component/directive level, because it is already **provided in root**. + + +### Get Methods + +`EnvironmentService` has numerous get methods which allow you to get a specific value or all environment object. + +Get methods with "$" at the end of the method name (e.g. `getEnvironment$`) return an RxJs stream. The streams are triggered when set or patched the state. + +#### How to Get Environment Object + +You can use the `getEnvironment` or `getEnvironment$` method of `EnvironmentService` to get all of the environment object. It is used as follows: + +```js +// this.environment is instance of EnvironmentService + +const environment = this.environment.getAll(); + +// or +this.environment.getAll$().subscribe(environment => { + // use environment here +}) +``` + +#### How to Get API URL + +The `getApiUrl` or `getApiUrl$` method is used to get a specific API URL from the environment object. This is how you can use it: + +```js +// this.environment is instance of EnvironmentService + +const apiUrl = this.environment.getApiUrl(); +// environment.apis.default.url + +this.environment.getApiUrl$("search").subscribe(searchUrl => { +// environment.apis.search.url +}) +``` + +This method returns the `url` of a specific API based on the key given as its only parameter. If there is no key, `'default'` is used. + + +#### How to Set the Environment + +`EnvironmentService` has a method named `setState` which allow you to set the state value. + +```js +// this.environment is instance of EnvironmentService + +this.environment.setState(newEnvironmentObject); +``` + +Note that **you do not have to call this method at application initiation**, because the environment variables are already being stored at start. + +#### Environment Properties + +Please refer to `Environment` type for all the properties. It can be found in the [config.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L13). \ No newline at end of file diff --git a/docs/en/UI/Angular/Feature-Libraries.md b/docs/en/UI/Angular/Feature-Libraries.md index 937bae7245..bdfff93371 100644 --- a/docs/en/UI/Angular/Feature-Libraries.md +++ b/docs/en/UI/Angular/Feature-Libraries.md @@ -90,9 +90,3 @@ Depending on the library, the `.forLazy` static method may also receive some opt --- 1 _Libraries expect to work at a predefined path. Please check [how to patch a navigation element](./Modifying-the-Menu.md#how-to-patch-or-remove-a-navigation-element), if you want to use a different path from the default one (e.g. '/identity')._ [↩](#a-modify-route) - ---- - -## What's Next? - -- [Service Proxies](./Service-Proxies.md) diff --git a/docs/en/UI/Angular/Features.md b/docs/en/UI/Angular/Features.md index 930fc2e527..3278bfd988 100644 --- a/docs/en/UI/Angular/Features.md +++ b/docs/en/UI/Angular/Features.md @@ -31,7 +31,3 @@ const defaultLang = this.config.getFeature("Identity.TwoFactor"); ``` You can then check the value of the feature to perform your logic. Please note that **feature keys are case-sensitive**. - -## What's Next? - -- [Permission Management](./Permission-Management.md) diff --git a/docs/en/UI/Angular/Form-Validation.md b/docs/en/UI/Angular/Form-Validation.md new file mode 100644 index 0000000000..a178fa213c --- /dev/null +++ b/docs/en/UI/Angular/Form-Validation.md @@ -0,0 +1,198 @@ +# Form Validation + +Reactive forms in ABP Angular UI are validated by [ngx-validate](https://www.npmjs.com/package/@ngx-validate/core) and helper texts are shown automatically based on validation rules and error blueprints. You do not have to add any elements or components to your templates. The library handles that for you. Here is how the experience is: + +The ngx-validate library validates an Angular reactive form and an error text appears under each wrong input based on the validation rule and the error blueprint. + +## How to Add New Error Messages + +You can add a new error message by passing validation options to the `ThemeSharedModule` in your root module. + +```js +import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; +import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; + +@NgModule({ + imports: [ + ThemeSharedModule.forRoot({ + validation: { + blueprints: { + uniqueUsername: "::AlreadyExists[{%{{{ username }}}%}]", + }, + }, + + // rest of theme shared config + }), + + // other imports + ], + + // rest of the module metadata +}) +export class AppModule {} +``` + +Alternatively, you may provide the `VALIDATION_BLUEPRINTS` token directly in your root module. Please do not forget to spread `DEFAULT_VALIDATION_BLUEPRINTS`. Otherwise, built-in ABP validation messages will not work. + +```js +import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; +import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; + +@NgModule({ + providers: [ + { + provide: VALIDATION_BLUEPRINTS, + useValue: { + ...DEFAULT_VALIDATION_BLUEPRINTS, + uniqueUsername: "::AlreadyExists[{%{{{ username }}}%}]", + }, + }, + + // other providers + ], + + // rest of the module metadata +}) +export class AppModule {} +``` + +When a [validator](https://angular.io/guide/form-validation#defining-custom-validators) or an [async validator](https://angular.io/guide/form-validation#creating-asynchronous-validators) returns an error with the key given to the error blueprints (`uniqueUsername` here), the validation library will be able to display an error message after localizing according to the given key and interpolation params. The result will look like this: + +An already taken username is entered while creating new user and a custom error message appears under the input after validation. + +In this example; + +- Localization key is `::AlreadyExists`. +- The interpolation param is `username`. +- Localization resource is defined as `"AlreadyExists": "Sorry, “{0}” already exists."`. +- And the validator should return `{ uniqueUsername: { username: "admin" } }` as the error object. + +## How to Change Existing Error Messages + +You can overwrite an existing error message by passing validation options to the `ThemeSharedModule` in your root module. Let's imagine you have a custom localization resource for required inputs. + +```json +"RequiredInput": "Oops! We need this input." +``` + +To use this instead of the built-in required input message, all you need to do is the following. + +```js +import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; +import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; + +@NgModule({ + imports: [ + ThemeSharedModule.forRoot({ + validation: { + blueprints: { + required: "::RequiredInput", + }, + }, + + // rest of theme shared config + }), + + // other imports + ], + + // rest of the module metadata +}) +export class AppModule {} +``` + +Alternatively, you may provide the `VALIDATION_BLUEPRINTS` token directly in your root module. Please do not forget to spread `DEFAULT_VALIDATION_BLUEPRINTS`. Otherwise, built-in ABP validation messages will not work. + +```js +import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; +import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; + +@NgModule({ + providers: [ + { + provide: VALIDATION_BLUEPRINTS, + useValue: { + ...DEFAULT_VALIDATION_BLUEPRINTS, + required: "::RequiredInput", + }, + }, + + // other providers + ], + + // rest of the module metadata +}) +export class AppModule {} +``` + +The error message will look like this: + +A required field is cleared and the custom error message appears under the input. + +## How to Disable Validation on a Form + +If you want to validate a form manually, you can always disable automatic validation on it. All you need to do is place `skipValidation` on the form element. + +```html +
+ +
+``` + +## How to Disable Validation on a Specific Field + +Validation works on any element or component with a `formControl` or `formControlName` directive. You can disable automatic validation on a specific field by placing `skipValidation` on the input element or component. + +```html + +``` + +## How to Use a Custom Error Component + +First, build a custom error component. Extending the existing `ValidationErrorComponent` would make it easier. + +```js +import { ValidationErrorComponent } from "@abp/ng.theme.basic"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "app-validation-error", + template: ` +
+ {%{{{ error.message | abpLocalization: error.interpoliteParams }}}%} +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ErrorComponent extends ValidationErrorComponent {} +``` + +Then, declare and provide it in your root module. + +```js +import { VALIDATION_ERROR_TEMPLATE } from "@ngx-validate/core"; + +@NgModule({ + // rest of the module metadata + + declarations: [ + // other declarables + ErrorComponent, + ], + providers: [ + // other providers + { + provide: VALIDATION_ERROR_TEMPLATE, + useValue: ErrorComponent, + }, + ], +}) +export class AppModule {} +``` + +The error message will be bold and italic now: + +A required field is cleared and a bold and italic error message appears. diff --git a/docs/en/UI/Angular/HTTP-Requests.md b/docs/en/UI/Angular/HTTP-Requests.md index 2ac5aa1b49..ab1ac04097 100644 --- a/docs/en/UI/Angular/HTTP-Requests.md +++ b/docs/en/UI/Angular/HTTP-Requests.md @@ -203,7 +203,3 @@ getSomeCustomHeaderValue() { You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10). - -## What's Next? - -* [Localization](./Localization.md) diff --git a/docs/en/UI/Angular/Lazy-Load-Service.md b/docs/en/UI/Angular/Lazy-Load-Service.md index 344b446107..0e1f7928c7 100644 --- a/docs/en/UI/Angular/Lazy-Load-Service.md +++ b/docs/en/UI/Angular/Lazy-Load-Service.md @@ -204,10 +204,3 @@ load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Obser - `strategy` parameter is the primary focus here and is explained above. - `retryTimes` defines how many times the loading will be tried again before fail (_default: 2_). - `retryDelay` defines how much delay there will be between retries (_default: 1000_). - - - - -## What's Next? - -- [Projecting Angular Content](./Content-Projection-Service.md) diff --git a/docs/en/UI/Angular/List-Service.md b/docs/en/UI/Angular/List-Service.md index d534558363..03e2b5341a 100644 --- a/docs/en/UI/Angular/List-Service.md +++ b/docs/en/UI/Angular/List-Service.md @@ -1,6 +1,6 @@ # Working with Lists -`ListService` is a utility service to provide an easy pagination, sorting, and search implementation. +`ListService` is a utility service to provide easy pagination, sorting, and search implementation. @@ -35,7 +35,10 @@ class BookComponent { constructor( public readonly list: ListService, private bookService: BookService, - ) {} + ) { + // change ListService defaults here + this.list.maxResultCount = 20; + } ngOnInit() { // A function that gets query and returns an observable @@ -68,6 +71,50 @@ Bind `ListService` to ngx-datatable like this: ``` +## Extending query with custom variables + +You can extend the query parameter of the `ListService`'s `hookToQuery` method. + +Firstly, you should pass your own type to `ListService` as shown below: + +```typescript +constructor(public readonly list: ListService) { } +``` + +Then update the `bookStreamCreator` constant like following: + +```typescript +const bookStreamCreator = (query) => this.bookService.getList({...query, name: 'name here'}); +``` + +You can also create your params object. + +Define a variable like this: + +```typescript +booksSearchParams = {} as BooksSearchParamsDto; +``` + +Update the `bookStreamCreator` constant: + +```typescript +const bookStreamCreator = (query) => this.bookService.getList({...query, ...this.booksSearchParams}); +``` + +Then you can place inputs to the HTML: + +```html +
+ +
+``` + +`ListService` emits the hookToQuery stream when you call the `this.list.get()` method. ## Usage with Observables @@ -120,7 +167,7 @@ You may use observables in combination with [AsyncPipe](https://angular.io/guide ``` -> We do not recommend using NGXS store for CRUD pages, unless your application needs to share list information between components or use it later on in another page. +> We do not recommend using the NGXS store for CRUD pages unless your application needs to share list information between components or use it later on in another page. ## How to Refresh Table on Create/Update/Delete @@ -142,7 +189,7 @@ You may use observables in combination with [AsyncPipe](https://angular.io/guide this.store.dispatch(new DeleteBook(id)).subscribe(this.list.get); ``` -> We donot recommend using NGXS store for CRUD pages, unless your application needs to share list information between components or use it later on in another page. +> We do not recommend using the NGXS store for CRUD pages unless your application needs to share list information between components or use it later on in another page. ## How to Implement Server-Side Search in a Table @@ -166,7 +213,7 @@ We had to modify the `ListService` to make it work with `ngx-datatable`. Previou > ``` -As of v3.0, with ngx-datatable, the `page` property has to be set as `0` for inital page. Therefore, if you used `ListService` on your tables before and are going to keep `abp-table`, you need to make the following change: +As of v3.0, with ngx-datatable, the `page` property has to be set as `0` for the initial page. Therefore, if you used `ListService` on your tables before and are going to keep `abp-table`, you need to make the following change: ```html @@ -177,8 +224,3 @@ As of v3.0, with ngx-datatable, the `page` property has to be set as `0` for ini ``` **Important Note:** The `abp-table` is not removed, but is deprecated and will be removed in the future. Please consider switching to ngx-datatable. - - -## What's Next? - -- [Easy *ngFor trackBy](./Track-By-Service.md) diff --git a/docs/en/UI/Angular/Localization.md b/docs/en/UI/Angular/Localization.md index 1efdbca359..6c7e6084a6 100644 --- a/docs/en/UI/Angular/Localization.md +++ b/docs/en/UI/Angular/Localization.md @@ -100,36 +100,6 @@ this.localizationService.get('Resource::Key'); this.localizationService.get({ key: 'Resource::Key', defaultValue: 'Default Value' }); ``` -### Using the Config State - -In order to you `getLocalization` method you should import ConfigState. - -```js -import { ConfigState } from '@abp/ng.core'; -``` - -Then you can use it as followed: - -```js -this.store.selectSnapshot(ConfigState.getLocalization('ResourceName::Key')); -``` - -`getLocalization` method can be used with both `localization key` and [`LocalizationWithDefault`](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L34) interface. - -```js -this.store.selectSnapshot( - ConfigState.getLocalization( - { - key: 'AbpIdentity::UserDeletionConfirmation', - defaultValue: 'Default Value', - }, - 'John', - ), -); -``` - -Localization resources are stored in the `localization` property of `ConfigState`. - ## RTL Support As of v2.9 ABP has RTL support. If you are generating a new project with v2.9 and above, everything is set, you do not need to do any changes. If you are migrating your project from an earlier version, please follow the 2 steps below: @@ -193,48 +163,129 @@ import { Component } from '@angular/core'; export class AppComponent {} ``` -## Mapping of Culture Name to Angular Locale File Name +## Registering a New Locale + +Since ABP has more than one language, Angular locale files loads lazily using [Webpack's import function](https://webpack.js.org/api/module-methods/#import-1) to avoid increasing the bundle size and register to Angular core using the [`registerLocaleData`](https://angular.io/api/common/registerLocaleData) function. The chunks to be included in the bundle are specified by the [Webpack's magic comments](https://webpack.js.org/api/module-methods/#magic-comments) as hard-coded. Therefore a `registerLocale` function that returns Webpack `import` function must be passed to `CoreModule`. + +### registerLocaleFn + +`registerLocale` function that exported from `@abp/ng.core/locale` package is a higher order function that accepts `cultureNameLocaleFileMap` object and `errorHandlerFn` function as params and returns Webpack `import` function. A `registerLocale` function must be passed to the `forRoot` of the `CoreModule` as shown below: + +```js +// app.module.ts + +import { registerLocale } from '@abp/ng.core/locale'; +// if you have commercial license and the language management module, add the below import +// import { registerLocale } from '@volo/abp.ng.language-management/locale'; + + +@NgModule({ + imports: [ + // ... + CoreModule.forRoot({ + // ...other options, + registerLocaleFn: registerLocale( + // you can pass the cultureNameLocaleFileMap and errorHandlerFn as optionally + { + cultureNameLocaleFileMap: { 'pt-BR': 'pt' }, + errorHandlerFn: ({ resolve, reject, locale, error }) => { + // the error can be handled here + }, + }, + ) + }), + //... + ] +``` + + +### Mapping of Culture Name to Angular Locale File Name Some of the culture names defined in .NET do not match Angular locales. In such cases, the Angular app throws an error like below at runtime: ![locale-error](./images/locale-error.png) -If you see an error like this, you should pass the `cultureNameLocaleFileMap` property like below to CoreModule's forRoot static method. +If you see an error like this, you should pass the `cultureNameLocaleFileMap` property like below to the `registerLocale` function. ```js // app.module.ts +import { registerLocale } from '@abp/ng.core/locale'; +// if you have commercial license and the language management module, add the below import +// import { registerLocale } from '@volo/abp.ng.language-management/locale'; + + @NgModule({ imports: [ - // other imports - CoreModule.forRoot({ - // other options - cultureNameLocaleFileMap: { - "DotnetCultureName": "AngularLocaleFileName", - "pt-BR": "pt" // example - } - }) + // ... + CoreModule.forRoot({ + // ...other options, + registerLocaleFn: registerLocale( + { + cultureNameLocaleFileMap: { + "DotnetCultureName": "AngularLocaleFileName", + "pt-BR": "pt" // example + }, + }, + ) + }), //... ``` See [all locale files in Angular](https://github.com/angular/angular/tree/master/packages/common/locales). -## Adding new culture +### Adding a New Culture + +Add the below code to the `app.module.ts` by replacing `your-locale` placeholder with a correct locale name. ```js //app.module.ts -import { storeLocaleData } from '@abp/ng.core'; +import { storeLocaleData } from '@abp/ng.core/locale'; import( /* webpackChunkName: "_locale-your-locale-js"*/ /* webpackMode: "eager" */ '@angular/common/locales/your-locale.js' ).then(m => storeLocaleData(m.default, 'your-locale')); ``` -## See Also -* [Localization in ASP.NET Core](../../Localization.md) +...or a custom `registerLocale` function can be passed to the `CoreModule`: + +```js +// register-locale.ts + +import { differentLocales } from '@abp/ng.core'; +export function registerLocale(locale: string) { + return import( + /* webpackChunkName: "_locale-[request]"*/ + /* webpackInclude: /[/\\](en|fr).js/ */ + /* webpackExclude: /[/\\]global|extra/ */ + `@angular/common/locales/${differentLocales[locale] || locale}.js` + ) +} + +// app.module.ts + +import { registerLocale } from './register-locale'; + +@NgModule({ + imports: [ + // ... + CoreModule.forRoot({ + // ...other options, + registerLocaleFn: registerLocale + }), + //... + ] +``` -## What's Next? +After this custom `registerLocale` function, since the en and fr added to the `webpackInclude`, only en and fr locale files will be created as chunks: -* [Settings](./Settings.md) +![locale chunks](https://user-images.githubusercontent.com/34455572/98203212-acaa2100-1f44-11eb-85af-4eb66d296326.png) + +Which locale files you add to `webpackInclude` magic comment, they will be included in the bundle + + +## See Also + +* [Localization in ASP.NET Core](../../Localization.md) diff --git a/docs/en/UI/Angular/Migration-Guide-v3.md b/docs/en/UI/Angular/Migration-Guide-v3.md index 6a7b86efcb..f13b0c966f 100644 --- a/docs/en/UI/Angular/Migration-Guide-v3.md +++ b/docs/en/UI/Angular/Migration-Guide-v3.md @@ -470,8 +470,3 @@ Some interfaces have long been marked as deprecated and now they are removed. - Please check if you are still using [anything listed in this issue](https://github.com/abpframework/abp/issues/4281) - -## What's Next? - -* [Quick Start](./Quick-Start.md) - diff --git a/docs/en/UI/Angular/Modifying-the-Menu.md b/docs/en/UI/Angular/Modifying-the-Menu.md index a3c356891e..b0a3b760fa 100644 --- a/docs/en/UI/Angular/Modifying-the-Menu.md +++ b/docs/en/UI/Angular/Modifying-the-Menu.md @@ -271,8 +271,3 @@ export class AppComponent { * Patched the languages dropdown element with new `requiredPolicy` and new `order`. * Removed the current user dropdown element. - - -## What's Next - -* [Component Replacement](./Component-Replacement.md) diff --git a/docs/en/UI/Angular/Multi-Tenancy.md b/docs/en/UI/Angular/Multi-Tenancy.md index c3e54c5f83..7622db6de0 100644 --- a/docs/en/UI/Angular/Multi-Tenancy.md +++ b/docs/en/UI/Angular/Multi-Tenancy.md @@ -1,4 +1,4 @@ -# Multi Tenancy in Angular UI +# Multi-Tenancy in Angular UI ABP Angular UI supports the multi-tenancy. The following features related to multi-tenancy are available in the startup templates. @@ -8,7 +8,7 @@ ABP Angular UI supports the multi-tenancy. The following features related to mul On the page above, you can; -- See the all tenants. +- See all tenants. - Create a new tenant. - Edit an existing tenant. - Delete a tenant. @@ -17,9 +17,11 @@ On the page above, you can;

Tenant Switching Component

-You can switch between existing tenants by using the tenant switching component in the child pages of the `AccountLayoutComponent` (like Login page). Angular UI sends the selected tenant id to the backend as `__tenant` header on each request. +You can switch between existing tenants by using the tenant switching box in the child pages of the MVC Account Public Module (like Login page). Angular UI gets selected tenant from `application-configuration` response and sends the tenant id to the backend as `__tenant` header on each request. -## Domain Tenant Resolver +## Domain/Subdomain Tenant Resolver + +> **Note:** If you are going to implement the steps below, you should also implement the domain/subdomain tenant resolver feature for the backend. See the [Domain/Subdomain Tenant Resolver section in Multi-Tenancy document](../../Multi-Tenancy#domain-subdomain-tenant-resolver) to learn the backend implementation. Angular UI can get the tenant name from the app running URL. You can determine the current tenant by subdomain (like mytenant1.mydomain.com) or by the whole domain (like mytenant.com). To do this, you need to set the `application.baseUrl` property in the environment: @@ -126,7 +128,3 @@ The app sends the `__tenant` header that contains the current tenant id on each ## See Also - [Multi Tenancy in ABP](../../Multi-Tenancy.md) - -## What's Next? - -- [Managing RxJS Subscriptions](./Subscription-Service.md) diff --git a/docs/en/UI/Angular/PWA-Configuration.md b/docs/en/UI/Angular/PWA-Configuration.md index a6aee244ba..918aa4970d 100644 --- a/docs/en/UI/Angular/PWA-Configuration.md +++ b/docs/en/UI/Angular/PWA-Configuration.md @@ -340,7 +340,3 @@ In case you want to cache other static files, please refer to the [service worke ### 3.2 Set Data Groups This part is unique to your project. We recommend being very careful about which endpoints to cache. Please refer to [service worker configuration document](https://angular.io/guide/service-worker-config#datagroups) on Angular.io for details. - -## What's Next? - -- [Config State](./Config-State.md) diff --git a/docs/en/UI/Angular/Page-Alerts.md b/docs/en/UI/Angular/Page-Alerts.md new file mode 100644 index 0000000000..a27cee4abf --- /dev/null +++ b/docs/en/UI/Angular/Page-Alerts.md @@ -0,0 +1,64 @@ +# Page Alerts + +A page alert is useful for displaying an important message to the user. The ABP Framework provides an easy way to show the following alert to the user. + +![angular-page-alert-example](./images/page-alert-warning-example.png) + +You can simply import `PageAlertService` from `@abp/ng.theme.shared` and utilize it as follows: + +```js +import { PageAlertService } from '@abp/ng.theme.shared'; + +@Component({ + // ... +}) +export class MyComponent { + constructor(private service: PageAlertService) {} + + showWarning() { + this.service.show({ + type: 'warning', + message: + 'We will have a service interruption between 02:00 AM and 04:00 AM at October 23, 2023!', + title: 'Service Interruption', + }); + } +} +``` + +## `SHOW` + +The method `show` accepts a single object that is type of `PageAlert` + +```js +export interface PageAlert { + type: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark'; + message: string; + title?: string; + dismissible?: boolean; + messageLocalizationParams?: string[]; + titleLocalizationParams?: string[]; +} +``` + +* `type` (Required): Defines what type of alert will be shown +* `message` (Required): The message who will be shown, also works with localization as well. +* `title` (Optional): The title of the message. If it is not provided, the title will be hidden. +* `dismissible` (Optional): Default is `true`. If enabled, a button on the top right corner will be shown to the users so that they can dismiss the message. +* `messageLocalizationParams` and `titleLocalizationParams` (Optional): If the message and/or the title is a key for localization service and contains some parameters, these fields could be used to pass those parameters. + +### An example with Localization + +```typescript +this.service.show({ + type: 'danger', + message: 'AbpAccount::PagerInfo{0}{1}{2}', + messageLocalizationParams: ['10', '20', '30'], + title: 'AbpAccount::EntityNotFoundErrorMessage', + titleLocalizationParams: ['Test', 'id123'], +}); +``` + +![angular-page-alert-with-params-example](./images/page-alert-with-params-example.png) + + diff --git a/docs/en/UI/Angular/Page-Toolbar-Extensions.md b/docs/en/UI/Angular/Page-Toolbar-Extensions.md new file mode 100644 index 0000000000..7e63950224 --- /dev/null +++ b/docs/en/UI/Angular/Page-Toolbar-Extensions.md @@ -0,0 +1,402 @@ +# Page Toolbar Extensions for Angular UI + +## Introduction + +Page toolbar extension system allows you to add a new action to the toolbar of a page. A "Click Me" action was added to the user management page below: + +Page Toolbar Extension Example: 'Click Me!' Action + +You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can also access to page data (the main record, usually an entity list) in your code. Additionally, you can pass in custom components instead of using the default button. + +## How to Add an Action to Page Toolbar + +In this example, we will add a "Click Me!" action and log `userName` of all users in the user management page of the [Identity Module](../../Modules/Identity.md) to the console. + +### Step 1. Create Toolbar Action Contributors + +The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root module: + +```js +// src/app/toolbar-action-contributors.ts + +import { + eIdentityComponents, + IdentityToolbarActionContributors, + IdentityUserDto, +} from '@abp/ng.identity'; +import { ToolbarAction, ToolbarActionList } from '@abp/ng.theme.shared/extensions'; + +const logUserNames = new ToolbarAction({ + text: 'Click Me!', + action: data => { + // Replace log with your custom code + data.record.forEach(user => console.log(user.userName)); + }, + // See ToolbarActionOptions in API section for all options +}); + +export function logUserNamesContributor(actionList: ToolbarActionList) { + actionList.addHead(logUserNames); +} + +export const identityToolbarActionContributors: IdentityToolbarActionContributors = { + // enum indicates the page to add contributors to + [eIdentityComponents.Users]: [ + logUserNamesContributor, + // You can add more contributors here + ], +}; + +``` + +The list of actions, conveniently named as `actionList`, is a **doubly linked list**. That is why we have used the `addHead` method, which adds the given value to the beginning of the list. You may find [all available methods here](../Common/Utils/Linked-List.md). + +### Step 2. Import and Use Toolbar Action Contributors + +Import `identityToolbarActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below: + +```js +// src/app/app-routing.module.ts + +// other imports +import { identityToolbarActionContributors } from './toolbar-action-contributors'; + +const routes: Routes = [ + // other routes + + { + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + toolbarActionContributors: identityToolbarActionContributors, + }) + ), + }, + + // other routes +]; +``` + +That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `IdentityModule`. + +## How to Add a Custom Component to Page Toolbar + +In this example, we will add a custom "Click Me!" button and log `userName` of all users in the user management page of the [Identity Module](../../Modules/Identity.md) to the console. + +Page Toolbar Extension Example: Custom Component + +### Step 1. Create A Custom Component + +We need to have a component before we can pass it to the toolbar action contributors: + +```js +// src/app/click-me-button.component.ts + +import { IdentityUserDto } from '@abp/ng.identity'; +import { ActionData, EXTENSIONS_ACTION_DATA } from '@abp/ng.theme.shared/extensions'; +import { Component, Inject } from '@angular/core'; + +@Component({ + selector: 'app-click-me-button', + template: ``, +}) +export class ClickMeButtonComponent { + constructor( + @Inject(EXTENSIONS_ACTION_DATA) + private data: ActionData + ) {} + + handleClick() { + this.data.record.forEach(user => console.log(user.userName)); + } +} + +``` + +Here, `EXTENSIONS_ACTION_DATA` token provides us the context from the page toolbar. Therefore, we are able to reach the page data via `record`, which is an array of users, i.e. `IdentityUserDto[]`. + +> We could also import `EXTENSIONS_ACTION_CALLBACK` from **@abp/ng.theme.shared/extensions** package, which is a higher order function that triggers the predefined `action` when called. It passes `ActionData` as the first parameter, so you do not have to pass it explicitly. In other words, `EXTENSIONS_ACTION_CALLBACK` can be called without any parameters and it will not fail. + +### Step 2. Create Toolbar Action Contributors + +The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root module. When `ToolbarComponent` is used instead of `ToolbarAction`, we can pass a component in: + +```js +// src/app/toolbar-action-contributors.ts + +import { + eIdentityComponents, + IdentityToolbarActionContributors, + IdentityUserDto, +} from '@abp/ng.identity'; +import { ToolbarActionList, ToolbarComponent } from '@abp/ng.theme.shared/extensions'; +import { ClickMeButtonComponent } from './click-me-button.component'; + +const logUserNames = new ToolbarComponent({ + component: ClickMeButtonComponent, + // See ToolbarActionOptions in API section for all options +}); + +export function logUserNamesContributor(actionList: ToolbarActionList) { + actionList.addHead(logUserNames); +} + +export const identityToolbarActionContributors: IdentityToolbarActionContributors = { + // enum indicates the page to add contributors to + [eIdentityComponents.Users]: [ + logUserNamesContributor, + // You can add more contributors here + ], +}; + +``` + +The list of actions, conveniently named as `actionList`, is a **doubly linked list**. That is why we have used the `addHead` method, which adds the given value to the beginning of the list. You may find [all available methods here](../Common/Utils/Linked-List.md). + +### Step 3. Import and Use Toolbar Action Contributors + +Import `identityToolbarActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below. + +```js +// src/app/app-routing.module.ts + +// other imports +import { identityToolbarActionContributors } from './toolbar-action-contributors'; + +const routes: Routes = [ + // other routes + + { + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + toolbarActionContributors: identityToolbarActionContributors, + }) + ), + }, + + // other routes +]; +``` + +That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `IdentityModule` and it will be triggered by a custom button, i.e. `ClickMeButtonComponent`. Please note that **component projection is not limited to buttons** and you may use other UI components. + +## How to Place a Custom Modal and Trigger It by Toolbar Actions + +Please check the same topic in [entity action extensions document](Entity-Action-Extensions.md) and replace entity action with a toolbar action. + +## API + +### ActionData\ + +`ActionData` is the shape of the parameter passed to all callbacks or predicates in a `ToolbarAction`. + +It has the following properties: + +- **record** is the page data, the main record on a page, usually an entity list (e.g. list of users). + + ```js + { + text: 'Click Me!', + action: data => { + data.record.forEach(user => { + console.lof(user.userName); + }); + }, + } + ``` + +- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `PageToolbarComponent`, including, but not limited to, its parent component. + + ```js + { + text: 'Click Me!', + action: data => { + const restService = data.getInjected(RestService); + + // Use restService public props and methods here + }, + visible: data => { + const usersComponent = data.getInjected(UsersComponent); + + // Use usersComponent public props and methods here + }, + } + ``` + +### ActionCallback\ + +`ActionCallback` is the type of the callback function that can be passed to a `ToolbarAction` as `action` parameter. An action callback gets a single parameter, the `ActionData`. The return type may be anything, including `void`. Here is a simplified representation: + +```js +type ActionCallback = (data?: ActionData) => R; +``` + +### ActionPredicate\ + +`ActionPredicate` is the type of the predicate function that can be passed to a `ToolbarAction` as `visible` parameter. An action predicate gets a single parameter, the `ActionData`. The return type must be `boolean`. Here is a simplified representation: + +```js +type ActionPredicate = (data?: ActionData) => boolean; +``` + +### ToolbarActionOptions\ + +`ToolbarActionOptions` is the type that defines required and optional properties you have to pass in order to create an toolbar action. + +Its type definition is as follows: + +```js +type ToolbarActionOptions = { + action: ActionCallback, + text: string, + icon?: string, + permission?: string, + visible?: ActionPredicate, +}; +``` + +As you see, passing `action` and `text` is enough to create an toolbar action. Here is what each property is good for: + +- **action** is a callback that is called when the toolbar action is clicked. (_required_) +- **text** is the button text which will be localized. (_required_) +- **icon** is the classes that define an icon to be placed before the text. (_default:_ `''`) +- **permission** is the permission context which will be used to decide if this toolbar action should be displayed to the user or not. (_default:_ `undefined`) +- **visible** is a predicate that will be used to decide if the page toolbar should have this action or not. (_default:_ `() => true`) + +You may find a full example below. + +### ToolbarAction\ + +`ToolbarAction` is the class that defines your toolbar actions. It takes an `ToolbarActionOptions` and sets the default values to the properties, creating an toolbar action that can be passed to an toolbar contributor. + +```js +const options: ToolbarActionOptions = { + action: data => { + const service = data.getInjected(MyCustomIdentityService); + const lockedUsers = data.record.filter(user => user.isLockedOut); + service.unlockAll(lockedUsers); + }, + text: 'MyProjectName::UnlockAll', + icon: 'fa fa-unlock', + permission: 'AbpIdentity.Users.Update', + visible: data => data.record.some(user => user.isLockedOut), +}; + +const action = new ToolbarAction(options); +``` + +It also has two static methods to create its instances: + +- **ToolbarAction.create\\(options: ToolbarActionOptions\\)** is used to create an instance of `ToolbarAction`. + ```js + const action = ToolbarAction.create(options); + ``` +- **ToolbarAction.createMany\\(options: ToolbarActionOptions\\[\]\)** is used to create multiple instances of `ToolbarAction` with given array of `ToolbarActionOptions`. + +### ToolbarComponentOptions\ + +`ToolbarComponentOptions` is the type that defines required and optional properties you have to pass in order to create an toolbar component. + +Its type definition is as follows: + +```js +type ToolbarComponentOptions = { + component: Type, + action?: ActionCallback, + permission?: string, + visible?: ActionPredicate, +}; +``` + +As you see, passing `action` and `text` is enough to create an toolbar action. Here is what each property is good for: + +- **component** is the constructor of the component to be projected. (_required_) +- **action** is a predefined callback that you can reach in your component via `EXTENSIONS_ACTION_CALLBACK` token and trigger. (_optional_) +- **permission** is the permission context which will be used to decide if this toolbar action should be displayed to the user or not. (_default:_ `undefined`) +- **visible** is a predicate that will be used to decide if the page toolbar should have this action or not. (_default:_ `() => true`) + +You may find a full example below. + +### ToolbarComponent\ + +`ToolbarComponent` is the class that defines toolbar actions which project a custom component. It takes an `ToolbarComponentOptions` and sets the default values to the properties, creating a toolbar action that can be passed to an toolbar contributor. + +```js +const options: ToolbarComponentOptions = { + component: UnlockAllButton, + action: data => { + const service = data.getInjected(MyCustomIdentityService); + const lockedUsers = data.record.filter(user => user.isLockedOut); + service.unlockAll(lockedUsers); + }, + permission: 'AbpIdentity.Users.Update', + visible: data => data.record.some(user => user.isLockedOut), +}; + +const action = new ToolbarComponent(options); +``` + +It also has two static methods to create its instances: + +- **ToolbarComponent.create\\(options: ToolbarComponentOptions\\)** is used to create an instance of `ToolbarComponent`. + ```js + const action = ToolbarComponent.create(options); + ``` +- **ToolbarComponent.createMany\\(options: ToolbarComponentOptions\\[\]\)** is used to create multiple instances of `ToolbarComponent` with given array of `ToolbarComponentOptions`. + ```js + const actions = ToolbarComponent.createMany(optionsArray); + ``` + +### ToolbarActionList\ + +`ToolbarActionList` is the list of actions passed to every action contributor callback as the first parameter named `actionList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md). + +The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this: + +```js +export function reorderUserContributors( + actionList: ToolbarActionList, +) { + // drop "New User" button + const newUserActionNode = actionList.dropByValue( + 'AbpIdentity::NewUser', + (action, text) => action['text'] === text, + ); + + // add it back to the head of the list + actionList.addHead(newUserActionNode.value); +} + +export const identityEntityActionContributors = { + [eIdentityComponents.Users]: [ + logUserNamesContributor, + reorderUserContributors, + ], +}; +``` + +### ToolbarActionContributorCallback\ + +`ToolbarActionContributorCallback` is the type that you can pass as toolbar action contributor callbacks to static `forLazy` methods of the modules. + +```js +// exportUsersContributor should have ToolbarActionContributorCallback type + +export function exportUsersContributor( + actionList: ToolbarActionList, +) { + // add exportUsers just before the last action + actionList.add(exportUsers).byIndex(-1); +} + +export const identityEntityActionContributors = { + [eIdentityComponents.Users]: [exportUsersContributor], +}; +``` + +## See Also + +- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md) diff --git a/docs/en/UI/Angular/Permission-Management.md b/docs/en/UI/Angular/Permission-Management.md index b9c4649c03..b8125bb2a3 100644 --- a/docs/en/UI/Angular/Permission-Management.md +++ b/docs/en/UI/Angular/Permission-Management.md @@ -2,22 +2,46 @@ A permission is a simple policy that is granted or prohibited for a particular user, role or client. You can read more about [authorization in ABP](../../Authorization.md) document. -You can get permission of authenticated user using `getGrantedPolicy` selector of `ConfigState`. +You can get permission of authenticated user using `getGrantedPolicy` or `getGrantedPolicy$` method of `PermissionService`. + +> ConfigState's getGrantedPolicy selector and ConfigStateService's getGrantedPolicy method deprecated. Use permission service's `getGrantedPolicy$` or `getGrantedPolicy`methods instead You can get permission as boolean value: ```js -import { ConfigStateService } from '@abp/ng.core'; +import { PermissionService } from '@abp/ng.core'; export class YourComponent { - constructor(private config: ConfigStateService) {} + constructor(private permissionService: PermissionService) {} ngOnInit(): void { - const canCreate = this.config.getGrantedPolicy('AbpIdentity.Roles.Create'); + const canCreate = this.permissionService.getGrantedPolicy('AbpIdentity.Roles.Create'); } } ``` +You may also **combine policy keys** to fine tune your selection: + +```js +// this.permissionService is instance of PermissionService + +const hasIdentityAndAccountPermission = this.permissionService.getGrantedPolicy( + "Abp.Identity && Abp.Account" +); + +const hasIdentityOrAccountPermission = this.permissionService.getGrantedPolicy( + "Abp.Identity || Abp.Account" +); +``` + +Please consider the following **rules** when creating your permission selectors: + +- Maximum 2 keys can be combined. +- `&&` operator looks for both keys. +- `||` operator looks for either key. +- Empty string `''` as key will return `true` +- Using an operator without a second key will return `false` + ## Permission Directive You can use the `PermissionDirective` to manage visibility of a DOM Element accordingly to user's permission. @@ -30,8 +54,6 @@ You can use the `PermissionDirective` to manage visibility of a DOM Element acco As shown above you can remove elements from DOM with `abpPermission` structural directive. -The directive can also be used as an attribute directive but we recommend to you to use it as a structural directive. - ## Permission Guard You can use `PermissionGuard` if you want to control authenticated user's permission to access to the route during navigation. @@ -55,8 +77,4 @@ const routes: Routes = [ ]; ``` -Granted Policies are stored in the `auth` property of `ConfigState`. - -## What's Next? - -* [Multi Tenancy](./Multi-Tenancy.md) \ No newline at end of file +Granted Policies are stored in the `auth` property of `ConfigState`. \ No newline at end of file diff --git a/docs/en/UI/Angular/Quick-Start.md b/docs/en/UI/Angular/Quick-Start.md index a58671156c..c17f069c5f 100644 --- a/docs/en/UI/Angular/Quick-Start.md +++ b/docs/en/UI/Angular/Quick-Start.md @@ -204,9 +204,3 @@ In addition, you can [deploy your application to certain targets using the Angul --- 1 _The compiled output will be placed under `/dist` in a folder by the project name._ [↩](#a-dist-folder-name) - ---- - -## What's Next? - -- [Environment Variables](./Environment.md) diff --git a/docs/en/UI/Angular/Service-Proxies.md b/docs/en/UI/Angular/Service-Proxies.md index 92c4b01871..8f6b1c4d60 100644 --- a/docs/en/UI/Angular/Service-Proxies.md +++ b/docs/en/UI/Angular/Service-Proxies.md @@ -137,7 +137,3 @@ export class BookComponent implements OnInit { ``` > Please [see this article](https://github.com/abpframework/abp/blob/dev/docs/en/Blog-Posts/2020-09-07%20Angular-Service-Proxies/POST.md) to learn more about service proxies. - -## What's Next? - -- [PWA Configuration](./PWA-Configuration.md) diff --git a/docs/en/UI/Angular/Settings.md b/docs/en/UI/Angular/Settings.md index f3a5775838..0dd29025e5 100644 --- a/docs/en/UI/Angular/Settings.md +++ b/docs/en/UI/Angular/Settings.md @@ -53,7 +53,3 @@ const localizationSettings = this.config.getSettings("Localization"); ``` Beware though, **settings search is case-sensitive**. - -## What's Next? - -- [Features](./Features.md) diff --git a/docs/en/UI/Angular/Subscription-Service.md b/docs/en/UI/Angular/Subscription-Service.md index dee77cbf1e..e080f8f3fa 100644 --- a/docs/en/UI/Angular/Subscription-Service.md +++ b/docs/en/UI/Angular/Subscription-Service.md @@ -197,7 +197,3 @@ class DemoComponent implements OnInit { } } ``` - -## What's Next? - -- [Working with Lists](./List-Service.md) diff --git a/docs/en/UI/Angular/Testing.md b/docs/en/UI/Angular/Testing.md new file mode 100644 index 0000000000..509fbefe95 --- /dev/null +++ b/docs/en/UI/Angular/Testing.md @@ -0,0 +1,376 @@ +# Unit Testing Angular UI + +ABP Angular UI is tested like any other Angular application. So, [the guide here](https://angular.io/guide/testing) applies to ABP too. That said, we would like to point out some **unit testing topics specific to ABP Angular applications**. + +## Setup + +In Angular, unit tests use [Karma](https://karma-runner.github.io/) and [Jasmine](https://jasmine.github.io) by default. Although we like Jest more, we chose not to deviate from these defaults, so **the application template you download will have Karma and Jasmine preconfigured**. You can find the Karma configuration inside the _karma.conf.js_ file in the root folder. You don't have to do anything. Adding a spec file and running `npm test` will work. + +## Basics + +An over-simplified spec file looks like this: + +```js +import { CoreTestingModule } from "@abp/ng.core/testing"; +import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; +import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; +import { NgxValidateCoreModule } from "@ngx-validate/core"; +import { MyComponent } from "./my.component"; + +describe("MyComponent", () => { + let fixture: ComponentFixture; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [MyComponent], + imports: [ + CoreTestingModule.withConfig(), + ThemeSharedTestingModule.withConfig(), + ThemeBasicTestingModule.withConfig(), + NgxValidateCoreModule, + ], + providers: [ + /* mock providers here */ + ], + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(MyComponent); + fixture.detectChanges(); + }); + + it("should be initiated", () => { + expect(fixture.componentInstance).toBeTruthy(); + }); +}); +``` + +If you take a look at the imports, you will notice that we have prepared some testing modules to replace built-in ABP modules. This is necessary for providing mocks for some features which otherwise would break your tests. Please remember to **use testing modules** and **call their `withConfig` static method**. + +## Tips + +### Angular Testing Library + +Although you can test your code with Angular TestBed, you may find [Angular Testing Library](https://testing-library.com/docs/angular-testing-library/intro) a good alternative. + +The simple example above can be written with Angular Testing Library as follows: + +```js +import { CoreTestingModule } from "@abp/ng.core/testing"; +import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; +import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; +import { ComponentFixture } from "@angular/core/testing"; +import { NgxValidateCoreModule } from "@ngx-validate/core"; +import { render } from "@testing-library/angular"; +import { MyComponent } from "./my.component"; + +describe("MyComponent", () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + const result = await render(MyComponent, { + imports: [ + CoreTestingModule.withConfig(), + ThemeSharedTestingModule.withConfig(), + ThemeBasicTestingModule.withConfig(), + NgxValidateCoreModule, + ], + providers: [ + /* mock providers here */ + ], + }); + + fixture = result.fixture; + }); + + it("should be initiated", () => { + expect(fixture.componentInstance).toBeTruthy(); + }); +}); +``` + +Very similar, as you can see. The real difference kicks in when we use queries and fire events. + +```js +// other imports +import { getByLabelText, screen } from "@testing-library/angular"; +import userEvent from "@testing-library/user-event"; + +describe("MyComponent", () => { + beforeEach(/* removed for sake of brevity */); + + it("should display advanced filters", () => { + const filters = screen.getByTestId("author-filters"); + const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement; + expect(nameInput.offsetWidth).toBe(0); + + const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i }); + userEvent.click(advancedFiltersBtn); + + expect(nameInput.offsetWidth).toBeGreaterThan(0); + + userEvent.type(nameInput, "fooo{backspace}"); + expect(nameInput.value).toBe("foo"); + }); +}); +``` + +The **queries in Angular Testing Library follow practices for maintainable tests**, the user event package provides a **human-like interaction** with the DOM, and the library in general has **a clear API** that simplifies component testing. Please find some useful links below: + +- [Queries](https://testing-library.com/docs/dom-testing-library/api-queries) +- [User Event](https://testing-library.com/docs/ecosystem-user-event) +- [Examples](https://github.com/testing-library/angular-testing-library/tree/master/apps/example-app/app/examples) + +### Clearing DOM After Each Spec + +One thing to remember is that Karma runs tests in real browser instances. That means, you will be able to see the result of your test code, but also have problems with components attached to the document body which may not get cleared after each test, even when you configure Karma to do so. + +We have prepared a simple function with which you can clear any leftover DOM elements after each test. + +```js +// other imports +import { clearPage } from "@abp/ng.core/testing"; + +describe("MyComponent", () => { + let fixture: ComponentFixture; + + afterEach(() => clearPage(fixture)); + + beforeEach(async () => { + const result = await render(MyComponent, { + /* removed for sake of brevity */ + }); + fixture = result.fixture; + }); + + // specs here +}); +``` + +Please make sure you use it because Karma will fail to remove dialogs otherwise and you will have multiple copies of modals, confirmation boxes, and alike. + +### Waiting + +Some components, modals, in particular, work off-detection-cycle. In other words, you cannot reach DOM elements inserted by these components immediately after opening them. Similarly, inserted elements are not immediately destroyed upon closing them. + +For this purpose, we have prepared a `wait` function. + +```js +// other imports +import { wait } from "@abp/ng.core/testing"; + +describe("MyComponent", () => { + beforeEach(/* removed for sake of brevity */); + + it("should open a modal", async () => { + const openModalBtn = screen.getByRole("button", { name: "Open Modal" }); + userEvent.click(openModalBtn); + + await wait(fixture); + + const modal = screen.getByRole("dialog"); + + expect(modal).toBeTruthy(); + + /* wait again after closing the modal */ + }); +}); +``` + +The `wait` function takes a second parameter, i.e. timeout (default: `0`). Try not to use it though. Using a timeout bigger than `0` is usually a signal that something is not quite right. + +## Testing Example + +Here is an example test suite. It doesn't cover all, but gives quite a good idea about what the testing experience will be like. + +```js +import { clearPage, CoreTestingModule, wait } from "@abp/ng.core/testing"; +import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; +import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; +import { ComponentFixture } from "@angular/core/testing"; +import { + NgbCollapseModule, + NgbDatepickerModule, + NgbDropdownModule, +} from "@ng-bootstrap/ng-bootstrap"; +import { NgxValidateCoreModule } from "@ngx-validate/core"; +import { CountryService } from "@proxy/countries"; +import { + findByText, + getByLabelText, + getByRole, + getByText, + queryByRole, + render, + screen, +} from "@testing-library/angular"; +import userEvent from "@testing-library/user-event"; +import { BehaviorSubject, of } from "rxjs"; +import { CountryComponent } from "./country.component"; + +const list$ = new BehaviorSubject({ + items: [{ id: "ID_US", name: "United States of America" }], + totalCount: 1, +}); + +describe("Country", () => { + let fixture: ComponentFixture; + + afterEach(() => clearPage(fixture)); + + beforeEach(async () => { + const result = await render(CountryComponent, { + imports: [ + CoreTestingModule.withConfig(), + ThemeSharedTestingModule.withConfig(), + ThemeBasicTestingModule.withConfig(), + NgxValidateCoreModule, + NgbCollapseModule, + NgbDatepickerModule, + NgbDropdownModule, + ], + providers: [ + { + provide: CountryService, + useValue: { + getList: () => list$, + }, + }, + ], + }); + + fixture = result.fixture; + }); + + it("should display advanced filters", () => { + const filters = screen.getByTestId("country-filters"); + const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement; + expect(nameInput.offsetWidth).toBe(0); + + const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i }); + userEvent.click(advancedFiltersBtn); + + expect(nameInput.offsetWidth).toBeGreaterThan(0); + + userEvent.type(nameInput, "fooo{backspace}"); + expect(nameInput.value).toBe("foo"); + + userEvent.click(advancedFiltersBtn); + expect(nameInput.offsetWidth).toBe(0); + }); + + it("should have a heading", () => { + const heading = screen.getByRole("heading", { name: "Countries" }); + expect(heading).toBeTruthy(); + }); + + it("should render list in table", async () => { + const table = await screen.findByTestId("country-table"); + + const name = getByText(table, "United States of America"); + expect(name).toBeTruthy(); + }); + + it("should display edit modal", async () => { + const actionsBtn = screen.queryByRole("button", { name: /actions/i }); + userEvent.click(actionsBtn); + + const editBtn = screen.getByRole("button", { name: /edit/i }); + userEvent.click(editBtn); + + await wait(fixture); + + const modal = screen.getByRole("dialog"); + const modalHeading = queryByRole(modal, "heading", { name: /edit/i }); + expect(modalHeading).toBeTruthy(); + + const closeBtn = getByText(modal, "×"); + userEvent.click(closeBtn); + + await wait(fixture); + + expect(screen.queryByRole("dialog")).toBeFalsy(); + }); + + it("should display create modal", async () => { + const newBtn = screen.getByRole("button", { name: /new/i }); + userEvent.click(newBtn); + + await wait(fixture); + + const modal = screen.getByRole("dialog"); + const modalHeading = queryByRole(modal, "heading", { name: /new/i }); + + expect(modalHeading).toBeTruthy(); + }); + + it("should validate required name field", async () => { + const newBtn = screen.getByRole("button", { name: /new/i }); + userEvent.click(newBtn); + + await wait(fixture); + + const modal = screen.getByRole("dialog"); + const nameInput = getByRole(modal, "textbox", { + name: /^name/i, + }) as HTMLInputElement; + + userEvent.type(nameInput, "x"); + userEvent.type(nameInput, "{backspace}"); + + const nameError = await findByText(modal, /required/i); + expect(nameError).toBeTruthy(); + }); + + it("should delete a country", () => { + const getSpy = spyOn(fixture.componentInstance.list, "get"); + const deleteSpy = jasmine.createSpy().and.returnValue(of(null)); + fixture.componentInstance.service.delete = deleteSpy; + + const actionsBtn = screen.queryByRole("button", { name: /actions/i }); + userEvent.click(actionsBtn); + + const deleteBtn = screen.getByRole("button", { name: /delete/i }); + userEvent.click(deleteBtn); + + const confirmText = screen.getByText("AreYouSure"); + expect(confirmText).toBeTruthy(); + + const confirmBtn = screen.getByRole("button", { name: "Yes" }); + userEvent.click(confirmBtn); + + expect(deleteSpy).toHaveBeenCalledWith(list$.value.items[0].id); + expect(getSpy).toHaveBeenCalledTimes(1); + }); +}); +``` + +## CI Configuration + +You would need a different configuration for your CI environment. To set up a new configuration for your unit tests, find the test project in _angular.json_ file and add one as seen below: + +```json +// angular.json + +"test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { /* several options here */ }, + "configurations": { + "production": { + "karmaConfig": "karma.conf.prod.js" + } + } +} +``` + +Now you can copy the _karma.conf.js_ as _karma.conf.prod.js_ and use any configuration you like in it. Please check [Karma configuration file document](http://karma-runner.github.io/5.2/config/configuration-file.html) for config options. + +Finally, don't forget to run your CI tests with the following command: + +```sh +npm test -- --prod +``` diff --git a/docs/en/UI/Angular/Toaster-Service.md b/docs/en/UI/Angular/Toaster-Service.md index 4d94b8cf6d..059fd720a8 100644 --- a/docs/en/UI/Angular/Toaster-Service.md +++ b/docs/en/UI/Angular/Toaster-Service.md @@ -249,7 +249,3 @@ Removes all open toasts. ## See Also - [Confirmation Popup](./Confirmation-Service.md) - -## What's Next? - -- [Modifying the Menu](./Modifying-the-Menu.md) diff --git a/docs/en/UI/Angular/Track-By-Service.md b/docs/en/UI/Angular/Track-By-Service.md index 7883be4754..a7df7b80b6 100644 --- a/docs/en/UI/Angular/Track-By-Service.md +++ b/docs/en/UI/Angular/Track-By-Service.md @@ -111,9 +111,3 @@ class DemoComponent { trackByTenantAccountId = trackByDeep('tenant', 'account', 'id'); } ``` - - - -## What's Next? - -- [Inserting Scripts & Styles to DOM](./Dom-Insertion-Service.md) diff --git a/docs/en/UI/Angular/images/entity-action-extensions---click-me.gif b/docs/en/UI/Angular/images/entity-action-extensions---click-me.gif new file mode 100644 index 0000000000..6f5191874d Binary files /dev/null and b/docs/en/UI/Angular/images/entity-action-extensions---click-me.gif differ diff --git a/docs/en/UI/Angular/images/entity-action-extensions---custom-modal.gif b/docs/en/UI/Angular/images/entity-action-extensions---custom-modal.gif new file mode 100644 index 0000000000..0b1010e029 Binary files /dev/null and b/docs/en/UI/Angular/images/entity-action-extensions---custom-modal.gif differ diff --git a/docs/en/UI/Angular/images/entity-prop-extensions---custom-cell.gif b/docs/en/UI/Angular/images/entity-prop-extensions---custom-cell.gif new file mode 100644 index 0000000000..2f3d79f701 Binary files /dev/null and b/docs/en/UI/Angular/images/entity-prop-extensions---custom-cell.gif differ diff --git a/docs/en/UI/Angular/images/entity-prop-extensions---name-column.gif b/docs/en/UI/Angular/images/entity-prop-extensions---name-column.gif new file mode 100644 index 0000000000..7c8be58759 Binary files /dev/null and b/docs/en/UI/Angular/images/entity-prop-extensions---name-column.gif differ diff --git a/docs/en/UI/Angular/images/form-prop-extensions---birthday-field.gif b/docs/en/UI/Angular/images/form-prop-extensions---birthday-field.gif new file mode 100644 index 0000000000..dd19f834e0 Binary files /dev/null and b/docs/en/UI/Angular/images/form-prop-extensions---birthday-field.gif differ diff --git a/docs/en/UI/Angular/images/form-validation---custom-error-template.gif b/docs/en/UI/Angular/images/form-validation---custom-error-template.gif new file mode 100644 index 0000000000..6d9f492ce4 Binary files /dev/null and b/docs/en/UI/Angular/images/form-validation---custom-error-template.gif differ diff --git a/docs/en/UI/Angular/images/form-validation---error-display-user-experience.gif b/docs/en/UI/Angular/images/form-validation---error-display-user-experience.gif new file mode 100644 index 0000000000..9070a0f730 Binary files /dev/null and b/docs/en/UI/Angular/images/form-validation---error-display-user-experience.gif differ diff --git a/docs/en/UI/Angular/images/form-validation---new-error-message.gif b/docs/en/UI/Angular/images/form-validation---new-error-message.gif new file mode 100644 index 0000000000..ae61215f61 Binary files /dev/null and b/docs/en/UI/Angular/images/form-validation---new-error-message.gif differ diff --git a/docs/en/UI/Angular/images/form-validation---overwrite-error-message.gif b/docs/en/UI/Angular/images/form-validation---overwrite-error-message.gif new file mode 100644 index 0000000000..b6af5a68b7 Binary files /dev/null and b/docs/en/UI/Angular/images/form-validation---overwrite-error-message.gif differ diff --git a/docs/en/UI/Angular/images/page-alert-warning-example.png b/docs/en/UI/Angular/images/page-alert-warning-example.png new file mode 100644 index 0000000000..253a811a22 Binary files /dev/null and b/docs/en/UI/Angular/images/page-alert-warning-example.png differ diff --git a/docs/en/UI/Angular/images/page-alert-with-params-example.png b/docs/en/UI/Angular/images/page-alert-with-params-example.png new file mode 100644 index 0000000000..8fc5236de3 Binary files /dev/null and b/docs/en/UI/Angular/images/page-alert-with-params-example.png differ diff --git a/docs/en/UI/Angular/images/toolbar-action-extensions---click-me.gif b/docs/en/UI/Angular/images/toolbar-action-extensions---click-me.gif new file mode 100644 index 0000000000..b6585184f1 Binary files /dev/null and b/docs/en/UI/Angular/images/toolbar-action-extensions---click-me.gif differ diff --git a/docs/en/UI/Angular/images/toolbar-action-extensions---custom-component.gif b/docs/en/UI/Angular/images/toolbar-action-extensions---custom-component.gif new file mode 100644 index 0000000000..9775dbd1f0 Binary files /dev/null and b/docs/en/UI/Angular/images/toolbar-action-extensions---custom-component.gif differ diff --git a/docs/en/UI/Angular/images/user-action-extension-click-me-ng.png b/docs/en/UI/Angular/images/user-action-extension-click-me-ng.png new file mode 100644 index 0000000000..675f05217a Binary files /dev/null and b/docs/en/UI/Angular/images/user-action-extension-click-me-ng.png differ diff --git a/docs/en/UI/Angular/images/user-page-toolbar-extension-click-me-ng.png b/docs/en/UI/Angular/images/user-page-toolbar-extension-click-me-ng.png new file mode 100644 index 0000000000..fcd36e4a7b Binary files /dev/null and b/docs/en/UI/Angular/images/user-page-toolbar-extension-click-me-ng.png differ diff --git a/docs/en/UI/Angular/images/user-page-toolbar-extension-custom-click-me-ng.png b/docs/en/UI/Angular/images/user-page-toolbar-extension-custom-click-me-ng.png new file mode 100644 index 0000000000..d8fd054eda Binary files /dev/null and b/docs/en/UI/Angular/images/user-page-toolbar-extension-custom-click-me-ng.png differ diff --git a/docs/en/UI/Angular/images/user-prop-extension-date-of-birth-field-ng.png b/docs/en/UI/Angular/images/user-prop-extension-date-of-birth-field-ng.png new file mode 100644 index 0000000000..948c4a9fd5 Binary files /dev/null and b/docs/en/UI/Angular/images/user-prop-extension-date-of-birth-field-ng.png differ diff --git a/docs/en/UI/Angular/images/user-prop-extension-name-column-ng.png b/docs/en/UI/Angular/images/user-prop-extension-name-column-ng.png new file mode 100644 index 0000000000..82d72b40ec Binary files /dev/null and b/docs/en/UI/Angular/images/user-prop-extension-name-column-ng.png differ diff --git a/docs/en/UI/AspNetCore/Basic-Theme.md b/docs/en/UI/AspNetCore/Basic-Theme.md index 3435e94a8b..67977ffd4e 100644 --- a/docs/en/UI/AspNetCore/Basic-Theme.md +++ b/docs/en/UI/AspNetCore/Basic-Theme.md @@ -1,3 +1,90 @@ # ASP.NET Core MVC / Razor Pages: The Basic Theme -This document is in progress. See the [Theming](Theming.md) document. \ No newline at end of file +The Basic Theme is a theme implementation for the ASP.NET Core MVC / Razor Pages UI. It is a minimalist theme that doesn't add any styling on top of the plain [Bootstrap](https://getbootstrap.com/). You can take the Basic Theme as the **base theme** and build your own theme or styling on top of it. See the *Customization* section. + +The Basic Theme has RTL (Right-to-Left language) support. + +> If you are looking for a professional, enterprise ready theme, you can check the [Lepton Theme](https://commercial.abp.io/themes), which is a part of the [ABP Commercial](https://commercial.abp.io/). + +> See the [Theming document](Theming.md) to learn about themes. + +## Installation + +**This theme is already installed** when you create a new solution using the [startup templates](../../Startup-Templates/Index.md). If you need to manually install it, follow the steps below: + +* Install the [Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic) NuGet package to your web project. +* Add `AbpAspNetCoreMvcUiBasicThemeModule` into the `[DependsOn(...)]` attribute for your [module class](../../Module-Development-Basics.md) in the web project. +* Install the [@abp/aspnetcore.mvc.ui.theme.basic](https://www.npmjs.com/package/@abp/aspnetcore.mvc.ui.theme.basic) NPM package to your web project (e.g. `npm install @abp/aspnetcore.mvc.ui.theme.basic` or `yarn add @abp/aspnetcore.mvc.ui.theme.basic`). +* Run `gulp` command in a command line terminal in the web project's folder. + +## Layouts + +The Basic Theme implements the standard layouts. All the layouts implement the following parts; + +* Global [Bundles](Bundling-Minification.md) +* [Page Alerts](Page-Alerts.md) +* [Layout Hooks](Layout-Hooks.md) +* [Widget](Widgets.md) Resources + +### The Application Layout + +![basic-theme-application-layout](../../images/basic-theme-application-layout.png) + +Application Layout implements the following parts, in addition to the common parts mentioned above; + +* Branding +* Main [Menu](Navigation-Menu.md) +* Main [Toolbar](Toolbars.md) with Language Selection & User Menu + +### The Account Layout + +![basic-theme-account-layout](../../images/basic-theme-account-layout.png) + +Application Layout implements the following parts, in addition to the common parts mentioned above; + +* Branding +* Main [Menu](Navigation-Menu.md) +* Main [Toolbar](Toolbars.md) with Language Selection & User Menu +* Tenant Switch Area + +### Empty Layout + +Empty layout is empty, as its name stands for. However, it implements the common parts mentioned above. + +## Customization + +You have two options two customize this theme: + +### Overriding Styles/Components + +In this approach, you continue to use the the theme as NuGet and NPM packages and customize the parts you need to. There are several ways to customize it; + +#### Override the Styles + +1. Create a CSS file in the `wwwroot` folder of your project: + +![example-global-styles](../../images/example-global-styles.png) + +2. Add the style file to the global bundle, in the `ConfigureServices` method of your [module](../../Module-Development-Basics.md): + +````csharp +Configure(options => +{ + options.StyleBundles.Configure(BasicThemeBundles.Styles.Global, bundle => + { + bundle.AddFiles("/styles/global-styles.css"); + }); +}); +```` + +#### Override the Components + +See the [User Interface Customization Guide](Customization-User-Interface.md) to learn how you can replace components, customize and extend the user interface. + +### Copy & Customize + +You can download the [source code](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic) of the Basic Theme, copy the project content into your solution, re-arrange the package/module dependencies (see the Installation section above to understand how it was installed to the project) and freely customize the theme based on your application requirements. + +## See Also + +* [Theming](Theming.md) \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Branding.md b/docs/en/UI/AspNetCore/Branding.md new file mode 100644 index 0000000000..aa1500401d --- /dev/null +++ b/docs/en/UI/AspNetCore/Branding.md @@ -0,0 +1,43 @@ +# ASP.NET Core MVC / Razor Pages: Branding + +## IBrandingProvider + +`IBrandingProvider` is a simple interface that is used to show the application name and logo on the layout. + +The screenshot below shows *MyProject* as the application name: + +![branding-nobrand](../../images/branding-nobrand.png) + +You can implement the `IBrandingProvider` interface or inherit from the `DefaultBrandingProvider` to set the application name: + +````csharp +using Volo.Abp.Ui.Branding; +using Volo.Abp.DependencyInjection; + +namespace MyProject.Web +{ + [Dependency(ReplaceServices = true)] + public class MyProjectBrandingProvider : DefaultBrandingProvider + { + public override string AppName => "Book Store"; + + public override string LogoUrl => "logo.png"; + } +} +```` + +The result will be like shown below: + +![bookstore-added-logo](../../images/bookstore-added-logo.png) + +`IBrandingProvider` has the following properties: + +* `AppName`: The application name. +* `LogoUrl`: A URL to show the application logo. +* `LogoReverseUrl`: A URL to show the application logo on a reverse color theme (dark, for example). + +> **Tip**: `IBrandingProvider` is used in every page refresh. For a multi-tenant application, you can return a tenant specific application name to customize it per tenant. + +## Overriding the Branding Area + +You can see the [UI Customization Guide](Customization-User-Interface.md) to learn how you can replace the branding area with a custom view component. diff --git a/docs/en/UI/AspNetCore/Breadcrumbs.md b/docs/en/UI/AspNetCore/Breadcrumbs.md deleted file mode 100644 index cc3bcd95be..0000000000 --- a/docs/en/UI/AspNetCore/Breadcrumbs.md +++ /dev/null @@ -1,3 +0,0 @@ -# Toolbars - -TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Bundling-Minification.md b/docs/en/UI/AspNetCore/Bundling-Minification.md index 9d88a37135..aeaea6c2e6 100644 --- a/docs/en/UI/AspNetCore/Bundling-Minification.md +++ b/docs/en/UI/AspNetCore/Bundling-Minification.md @@ -165,7 +165,27 @@ public class MyWebExtensionModule : AbpModule } ```` -> It's not possible to configure unnamed bundle tag helpers by code, because their name are not known at the development time. It's suggested to always use a name for a bundle tag helper. +You can also use the `ConfigureAll` method to configure all existing bundles: + +````C# +[DependsOn(typeof(MyWebModule))] +public class MyWebExtensionModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options + .ScriptBundles + .ConfigureAll(bundle => { + bundle.AddFiles( + "/scripts/my-extension-script.js" + ); + }); + }); + } +} +```` ## Bundle Contributors diff --git a/docs/en/UI/AspNetCore/Client-Side-Package-Management.md b/docs/en/UI/AspNetCore/Client-Side-Package-Management.md index 77dee7a383..645200a363 100644 --- a/docs/en/UI/AspNetCore/Client-Side-Package-Management.md +++ b/docs/en/UI/AspNetCore/Client-Side-Package-Management.md @@ -76,7 +76,8 @@ module.exports = { "@libs": "./wwwroot/libs" }, clean: [ - "@libs" + "@libs", + "!@libs/**/foo.txt" ], mappings: { @@ -85,7 +86,7 @@ module.exports = { ```` * **aliases** section defines standard aliases (placeholders) that can be used in the mapping paths. **@node_modules** and **@libs** are required (by the standard packages), you can define your own aliases to reduce duplication. -* **clean** section is a list of folders to clean before copying the files. +* **clean** section is a list of folders to clean before copying the files. Glob matching and negation is enabled, so you can fine-tune what to delete and keep. The example above will clean everything inside `./wwwroot/libs`, but keep any `foo.txt` files. * **mappings** section is a list of mappings of files/folders to copy. This example does not copy any resource itself, but depends on a standard package. An example mapping configuration is shown below: diff --git a/docs/en/UI/AspNetCore/Customization-User-Interface.md b/docs/en/UI/AspNetCore/Customization-User-Interface.md index c4e196fbb9..111a43ddcb 100644 --- a/docs/en/UI/AspNetCore/Customization-User-Interface.md +++ b/docs/en/UI/AspNetCore/Customization-User-Interface.md @@ -1,6 +1,6 @@ # ASP.NET Core (MVC / Razor Pages) User Interface Customization Guide -This document explains how to override the user interface of a depended [application module](../../Modules/Index.md) for ASP.NET Core MVC / Razor Page applications. +This document explains how to override the user interface of a depended [application module](../../Modules/Index.md) or [theme](Theming.md) for ASP.NET Core MVC / Razor Page applications. ## Overriding a Page @@ -28,15 +28,15 @@ namespace Acme.BookStore.Web.Pages.Identity.Users public class MyEditModalModel : EditModalModel { public MyEditModalModel( - IIdentityUserAppService identityUserAppService, + IIdentityUserAppService identityUserAppService, IIdentityRoleAppService identityRoleAppService ) : base( - identityUserAppService, + identityUserAppService, identityRoleAppService) { } - public override async Task OnPostAsync() + public async override Task OnPostAsync() { //TODO: Additional logic await base.OnPostAsync(); @@ -84,10 +84,10 @@ Create a page model class deriving from the ` LoginModel ` (defined in the ` Vol public class MyLoginModel : LoginModel { public MyLoginModel( - IAuthenticationSchemeProvider schemeProvider, + IAuthenticationSchemeProvider schemeProvider, IOptions accountOptions ) : base( - schemeProvider, + schemeProvider, accountOptions) { @@ -128,11 +128,11 @@ The ABP Framework, pre-built themes and modules define some **re-usable view com ### Example -The screenshot below was taken from the **basic theme** comes with the application startup template. +The screenshot below was taken from the [Basic Theme](Basic-Theme.md) comes with the application startup template. ![bookstore-brand-area-highlighted](../../images/bookstore-brand-area-highlighted.png) -[The basic theme](../../Themes/Basic.md) defines some view components for the layout. For example, the highlighted area with the red rectangle above is called **Brand component**. You probably want to customize this component by adding your **own application logo**. Let's see how to do it. +The [Basic Theme](Basic-Theme.md) defines some view components for the layout. For example, the highlighted area with the red rectangle above is called **Brand component**. You probably want to customize this component by adding your **own application logo**. Let's see how to do it. First, create your logo and place under a folder in your web application. We used `wwwroot/logos/bookstore-logo.png` path. Then copy the Brand component's view ([from here](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/Default.cshtml)) from the basic theme files under the `Themes/Basic/Components/Brand` folder. The result should be similar the picture below: @@ -437,7 +437,7 @@ See the layouts section below to learn more about the layout system. Layout system allows themes to define standard, named layouts and allows any page to select a proper layout for its purpose. There are three pre-defined layouts: -* "**Application**": The main (and the default) layout for an application. It typically contains header, menu (sidebar), footer, toolbar... etc. +* "**Application**": The main (and the default) layout for an application. It typically contains header, menu (sidebar), footer, toolbar... etc. * "**Account**": This layout is used by login, register and other similar pages. It is used for the pages under the `/Pages/Account` folder by default. * "**Empty**": Empty and minimal layout. diff --git a/docs/en/UI/AspNetCore/Data-Table-Column-Extensions.md b/docs/en/UI/AspNetCore/Data-Table-Column-Extensions.md new file mode 100644 index 0000000000..fec0ac71fb --- /dev/null +++ b/docs/en/UI/AspNetCore/Data-Table-Column-Extensions.md @@ -0,0 +1,161 @@ +# Data Table Column Extensions for ASP.NET Core UI + +## Introduction + +Data table column extension system allows you to add a **new table column** on the user interface. The example below adds a new column with the "Social security no" title: + +![user-action-extension-click-me](../../images/table-column-extension-example.png) + +You can use the standard column options to fine control the table column. + +> Note that this is a low level API to find control the table column. If you want to show an extension property on the table, see the [module entity extension](../../Module-Entity-Extensions.md) document. + +## How to Set Up + +### Create a JavaScript File + +First, add a new JavaScript file to your solution. We added inside the `/Pages/Identity/Users` folder of the `.Web` project: + +![user-action-extension-on-solution](../../images/user-action-extension-on-solution.png) + +Here, the content of this JavaScript file: + +```js +abp.ui.extensions.tableColumns + .get('identity.user') + .addContributor(function (columnList) { + columnList.addTail({ //add as the last column + title: 'Social security no', + data: 'extraProperties.SocialSecurityNumber', + orderable: false, + render: function (data, type, row) { + if (row.extraProperties.SocialSecurityNumber) { + return '' + + row.extraProperties.SocialSecurityNumber + + ''; + } else { + return 'undefined'; + } + } + }); + }); +``` + +This example defines a custom `render` function to return a custom HTML to render in the column. + +### Add the File to the User Management Page + +Then you need to add this JavaScript file to the user management page. You can take the power of the [Bundling & Minification system](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Bundling-Minification). + +Write the following code inside the `ConfigureServices` of your module class: + +```csharp +Configure(options => +{ + options.ScriptBundles.Configure( + typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName, + bundleConfiguration => + { + bundleConfiguration.AddFiles( + "/Pages/Identity/Users/my-user-extensions.js" + ); + }); +}); +``` + +This configuration adds `my-user-extensions.js` to the user management page of the Identity Module. `typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName` is the name of the bundle in the user management page. This is a common convention used for all the ABP Commercial modules. + +### Rendering the Column + +This example assumes that you've defined a `SocialSecurityNumber` extra property using the [module entity extension](../../Module-Entity-Extensions.md) system. However; + +* You can add a new column that is related to an existing property of the user (that was not added to the table by default). Example: + +````js +abp.ui.extensions.tableColumns + .get('identity.user') + .addContributor(function (columnList) { + columnList.addTail({ + title: 'Phone confirmed?', + data: 'phoneNumberConfirmed', + render: function (data, type, row) { + if (row.phoneNumberConfirmed) { + return 'YES'; + } else { + return 'NO'; + } + } + }); + }); +```` + +* You can add a new custom column that is not related to any entity property, but a completely custom information. Example: + +````js +abp.ui.extensions.tableColumns + .get('identity.user') + .addContributor(function (columnList) { + columnList.addTail({ + title: 'Custom column', + data: {}, + orderable: false, + render: function (data) { + if (data.phoneNumber) { + return "call: " + data.phoneNumber; + } else { + return ''; + } + } + }); + }); +```` + +## API + +This section explains details of the `abp.ui.extensions.tableColumns` JavaScript API. + +### abp.ui.extensions.tableColumns.get(entityName) + +This method is used to access the table columns for an entity of a specific module. It takes one parameter: + +* **entityName**: The name of the entity defined by the related module. + +### abp.ui.extensions.tableColumns.get(entityName).columns + +The `columns` property is used to retrieve a [doubly linked list](../Common/Utils/Linked-List.md) of previously defined columns for a table. All contributors are executed in order to prepare the final column list. This is normally called by the modules to show the columns in the table. However, you can use it if you are building your own extensible UIs. + +### abp.ui.extensions.tableColumns.get(entityName).addContributor(contributeCallback [, order]) + +The `addContributor` method covers all scenarios, e.g. you want to add your column in a different position in the list, change or remove an existing column. `addContributor` has the following parameters: + +* **contributeCallback**: A callback function that is called whenever the column list should be created. You can freely modify the column list inside this callback method. +* **order** (optional): The order of the callback in the callback list. Your callback is added to the end of the list (so, you have opportunity to modify columns added by the previous contributors). You can set it `0` to add your contributor as the first item. + +#### Example + +```js +var myColumnDefinition = { + title: 'Custom column', + data: {}, + orderable: false, + render: function(data) { + if (data.phoneNumber) { + return "call: " + data.phoneNumber; + } else { + return ''; + } + } +}; + +abp.ui.extensions.tableColumns + .get('identity.user') + .addContributor(function (columnList) { + // Remove an item from actionList + columnList.dropHead(); + + // Add a new item to the actionList + columnList.addHead(myColumnDefinition); + }); +``` + +> `columnList` is [linked list](../Common/Utils/Linked-List.md). You can use its methods to build a list of columns however you need. diff --git a/docs/en/UI/AspNetCore/Data-Tables.md b/docs/en/UI/AspNetCore/Data-Tables.md new file mode 100644 index 0000000000..b97d49d562 --- /dev/null +++ b/docs/en/UI/AspNetCore/Data-Tables.md @@ -0,0 +1,289 @@ +# ASP.NET Core MVC / Razor Pages: Data Tables + +A Data Table (aka Data Grid) is a UI component to show tabular data to the users. There are a lot of Data table components/libraries and **you can use any one you like** with the ABP Framework. However, the startup templates come with the [DataTables.Net](https://datatables.net/) library as **pre-installed and configured**. ABP Framework provides adapters for this library and make it easy to use with the API endpoints. + +An example screenshot from the user management page that shows the user list in a data table: + +![datatables-example](../../images/datatables-example.png) + +## DataTables.Net Integration + +First of all, you can follow the official documentation to understand how the [DataTables.Net](https://datatables.net/) works. This section will focus on the ABP addons & integration points rather than fully covering the usage of this library. + +### A Quick Example + +You can follow the [web application development tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC) for a complete example application that uses the DataTables.Net as the Data Table. This section shows a minimalist example. + +You do nothing to add DataTables.Net library to the page since it is already added to the global [bundle](Bundling-Minification.md) by default. + +First, add an `abp-table` as shown below, with an `id`: + +````html + +```` + +> `abp-table` is a [Tag Helper](Tag-Helpers/Index.md) defined by the ABP Framework, but a simple `` tag would also work. + +Then call the `DataTable` plugin on the table selector: + +````js +var dataTable = $('#BooksTable').DataTable( + abp.libs.datatables.normalizeConfiguration({ + serverSide: true, + paging: true, + order: [[1, "asc"]], + searching: false, + ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), + columnDefs: [ + { + title: l('Actions'), + rowAction: { + items: + [ + { + text: l('Edit'), + action: function (data) { + ///... + } + } + ] + } + }, + { + title: l('Name'), + data: "name" + }, + { + title: l('PublishDate'), + data: "publishDate", + render: function (data) { + return luxon + .DateTime + .fromISO(data, { + locale: abp.localization.currentCulture.name + }).toLocaleString(); + } + }, + { + title: l('Price'), + data: "price" + } + ] + }) +); +```` + +The example code above uses some ABP integration features those will be explained in the next sections. + +### Configuration Normalization + +`abp.libs.datatables.normalizeConfiguration` function takes a DataTables configuration and normalizes to simplify it; + +* Sets `scrollX` option to `true`, if not set. +* Sets `target` index for the column definitions. +* Sets the `language` option to [localize](../../Localization.md) the table in the current language. + +#### Default Configuration + +`normalizeConfiguration` uses the default configuration. You can change the default configuration using the `abp.libs.datatables.defaultConfigurations` object. Example: + +````js +abp.libs.datatables.defaultConfigurations.scrollX = false; +```` + +Here, the all configuration options; + +* `scrollX`: `false` by default. +* `dom`: Default value is `<"dataTable_filters"f>rt<"row dataTable_footer"<"col-auto"l><"col-auto"i><"col"p>>`. +* `language`: A function that returns the localization text using the current language. + +### AJAX Adapter + +DataTables.Net has its own expected data format while getting results of an AJAX call to the server to get the table data. They are especially related how paging and sorting parameters are sent and received. ABP Framework also offers its own conventions for the client-server [AJAX](JavaScript-API/Ajax.md) communication. + +The `abp.libs.datatables.createAjax` method (used in the example above) adapts request and response data format and perfectly works with the [Dynamic JavaScript Client Proxy](Dynamic-JavaScript-Proxies.md) system. + +This works automatically, so most of the times you don't need to know how it works. See the [DTO document](../../Data-Transfer-Objects.md) if you want to learn more about `IPagedAndSortedResultRequest`, `IPagedResult` and other standard interfaces and base DTO classes those are used in client to server communication. + +The `createAjax` also supports you to customize request parameters and handle the responses. + +**Example:** + +````csharp +var inputAction = function () { + return { + id: $('#Id').val(), + name: $('#Name').val(), + }; +}; + +var responseCallback = function(result) { + + // your custom code. + + return { + recordsTotal: result.totalCount, + recordsFiltered: result.totalCount, + data: result.items + }; +}; + +ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList, inputAction, responseCallback) +```` + +### Row Actions + +`rowAction` is an option defined by the ABP Framework to the column definitions to show a drop down button to take actions for a row in the table. + +The example screenshot below shows the actions for each user in the user management table: + +![datatables-example](../../images/datatables-row-actions.png) + +`rowAction` is defined as a part of a column definition: + +````csharp +{ + title: l('Actions'), + rowAction: { + //TODO: CONFIGURATION + } +}, +```` + +**Example: Show *Edit* and *Delete* actions for a book row** + +````js +{ + title: l('Actions'), + rowAction: { + items: + [ + { + text: l('Edit'), + action: function (data) { + //TODO: Open a modal to edit the book + } + }, + { + text: l('Delete'), + confirmMessage: function (data) { + return "Are you sure to delete the book " + data.record.name; + }, + action: function (data) { + acme.bookStore.books.book + .delete(data.record.id) + .then(function() { + abp.notify.info("Successfully deleted!"); + data.table.ajax.reload(); + }); + } + } + ] + } +}, +```` + +#### Action Items + +`items` is an array of action definitions. An action definition can have the following options; + +* `text`: The text (a `string`) for this action to be shown in the actions drop down. +* `action`: A `function` that is executed when the user clicks to the action. The function takes a `data` argument that has the following fields; + * `data.record`: This is the data object related to the row. You can access the data fields like `data.record.id`, `data.record.name`... etc. + * `data.table`: The DataTables instance. +* `confirmMessage`: A `function` (see the example above) that returns a message (`string`) to show a dialog to get a confirmation from the user before executing the `action`. Example confirmation dialog: + +![datatables-row-actions-confirmation](../../images/datatables-row-actions-confirmation.png) + +You can use the [localization](JavaScript-API/Localization.md) system to show a localized message. + +* `visible`: A `bool` or a `function` that returns a `bool`. If the result is `false`, then the action is not shown in the actions dropdown. This is generally combined by the [authorization](JavaScript-API/Auth.md) system to hide the action if the user has no permission to take this action. Example: + +````js +visible: abp.auth.isGranted('BookStore.Books.Delete'); +```` + +If you define a `function`, then the `function` has two arguments: `record` (the data object of the related row) and the `table` (the DataTable instance). So, you can decide to show/hide the action dynamically, based on the row data and other conditions. + +* `iconClass`: Can be used to show a font-icon, like a [Font-Awesome](https://fontawesome.com/) icon (ex: `fas fa-trash-alt`), near to the action text. Example screenshot: + +![datatables-row-actions-confirmation](../../images/datatables-row-actions-icon.png) + +* `enabled`: A `function` that returns a `bool` to disable the action. The `function` takes a `data` object with two fields: `data.record` is the data object related to the row and `data.table` is the DataTables instance. +* `displayNameHtml`: Set this to `true` is the `text` value contains HTML tags. + +There are some rules with the action items; + +* If none of the action items is visible then the actions column is not rendered. + +### Data Format + +#### The Problem + +See the *Creation Time* column in the example below: + +````js +{ + title: l('CreationTime'), + data: "creationTime", + render: function (data) { + return luxon + .DateTime + .fromISO(data, { + locale: abp.localization.currentCulture.name + }).toLocaleString(luxon.DateTime.DATETIME_SHORT); + } +} +```` + +The `render` is a standard DataTables option to render the column content by a custom function. This example uses the [luxon](https://moment.github.io/luxon/) library (which is installed by default) to write a human readable value of the `creationTime` in the current user's language. Example output of the column: + +![datatables-custom-render-date](../../images/datatables-custom-render-date.png) + +If you don't define the render option, then the result will be ugly and not user friendly: + +![datatables-custom-render-date](../../images/datatables-default-render-date.png) + +However, rendering a `DateTime` is almost same and repeating the same rendering logic everywhere is against to the DRY (Don't Repeat Yourself!) principle. + +#### dataFormat Option + +`dataFormat` column option specifies the data format that is used to render the column data. The same output could be accomplished using the following column definition: + +````js +{ + title: l('CreationTime'), + data: "creationTime", + dataFormat: 'datetime' +} +```` + +`dataFormat: 'datetime'` specifies the data format for this column. There are a few pre-defined `dataFormat`s: + +* `boolean`: Shows a `check` icon for `true` and `times` icon for `false` value and useful to render `bool` values. +* `date`: Shows date part of a `DateTime` value, formatted based on the current culture. +* `datetime`: Shows date & time (excluding seconds) of a `DateTime` value, formatted based on the current culture. + +### Default Renderers + +`abp.libs.datatables.defaultRenderers` option allows you to define new data formats and set renderers for them. + +**Example: Render male / female icons based on the gender** + +````js +abp.libs.datatables.defaultRenderers['gender'] = function(value) { + if (value === 'f') { + return ''; + } else { + return ''; + } +}; +```` + +Assuming that the possible values for a column data is `f` and `m`, the `gender` data format shows female/male icons instead of `f` and `m` texts. You can now set `dataFormat: 'gender'` for a column definition that has the proper data values. + +> You can write the default renderers in a single JavaScript file and add it to the [Global Script Bundle](Bundling-Minification.md), so you can reuse them in all the pages. + +## Other Data Grids + +You can use any library you like. For example, [see this article](https://community.abp.io/articles/using-devextreme-components-with-the-abp-framework-zb8z7yqv) to learn how to use DevExtreme Data Grid in your applications. diff --git a/docs/en/UI/AspNetCore/Dynamic-JavaScript-Proxies.md b/docs/en/UI/AspNetCore/Dynamic-JavaScript-Proxies.md index 391a63a910..3464d4bc68 100644 --- a/docs/en/UI/AspNetCore/Dynamic-JavaScript-Proxies.md +++ b/docs/en/UI/AspNetCore/Dynamic-JavaScript-Proxies.md @@ -1,3 +1,91 @@ -# Dynamic JavaScript HTTP API Proxies +# Dynamic JavaScript API Client Proxies -TODO \ No newline at end of file +It is typical to consume your HTTP APIs from your JavaScript code. To do that, you normally deal with low level AJAX calls, like $.ajax, or better [abp.ajax](JavaScript-API/Ajax.md). ABP Framework provides **a better way** to call your HTTP APIs from your JavaScript code: Dynamic JavaScript API Client Proxies! + +## A Quick Example + +Assume that you have an application service defined as shown below: + +````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 GetAsync(Guid id); + + Task> GetListAsync(GetAuthorListDto input); + + Task CreateAsync(CreateAuthorDto input); + + Task UpdateAsync(Guid id, UpdateAuthorDto input); + + Task DeleteAsync(Guid id); + } +} +```` + +> You can follow the [web application development tutorial](../../Tutorials/Part-1.md) to learn how to create [application services](../../Application-Services.md), expose them as [HTTP APIs](../../API/Auto-API-Controllers.md) and consume from the JavaScript code as a complete example. + +You can call any of the methods just like calling a JavaScript function. The JavaScript function has the identical function **name**, **parameters** and the **return value** with the C# method. + +**Example: Get the authors list** + +````js +acme.bookStore.authors.author.getList({ + maxResultCount: 10 +}).then(function(result){ + console.log(result.items); +}); +```` + +**Example: Delete an author** + +```js +acme.bookStore.authors.author + .delete('7245a066-5457-4941-8aa7-3004778775f0') //Get id from somewhere! + .then(function() { + abp.notify.info('Successfully deleted!'); + }); +``` + +## AJAX Details + +Dynamic JavaScript client proxy functions use the [abp.ajax](JavaScript-API/Ajax.md) under the hood. So, you have the same benefits like **automatic error handling**. Also, you can fully control the AJAX call by providing the options. + +### The Return Value + +Every function returns a [Deferred object](https://api.jquery.com/category/deferred-object/). That means you can chain with `then` to get the result, `catch` to handle the error, `always` to perform an action once the operation completes (success or failed). + +### AJAX Options + +Every function gets an additional **last parameter** after your own parameters. The last parameter is called as `ajaxParams`. It is an object that overrides the AJAX options. + +**Example: Set `type` and `dataType` AJAX options** + +````js +acme.bookStore.authors.author + .delete('7245a066-5457-4941-8aa7-3004778775f0', { + type: 'POST', + dataType: 'xml' + }) + .then(function() { + abp.notify.info('Successfully deleted!'); + }); +```` + +See the [jQuery.ajax](https://api.jquery.com/jQuery.ajax/) documentation for all the available options. + +## Service Proxy Script Endpoint + +The magic is done by the `/Abp/ServiceProxyScript` endpoint defined by the ABP Framework and automatically added to the layout. You can visit this endpoint in your application to see the client proxy function definitions. This script file is automatically generated by the ABP Framework based on the server side method definitions and the related HTTP endpoint details. + +## See Also + +* [Web Application Development Tutorial](../../Tutorials/Part-1.md) +* [Auto API Controllers](../../API/Auto-API-Controllers.md) +* [Dynamic C# API Client Proxies](../../API/Dynamic-CSharp-API-Clients.md) \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Entity-Action-Extensions.md b/docs/en/UI/AspNetCore/Entity-Action-Extensions.md new file mode 100644 index 0000000000..f6e865375d --- /dev/null +++ b/docs/en/UI/AspNetCore/Entity-Action-Extensions.md @@ -0,0 +1,108 @@ +# Entity Action Extensions for ASP.NET Core UI + +## Introduction + +Entity action extension system allows you to add a **new action** to the action menu for an entity. A **Click Me** action was added to the *User Management* page below: + +![user-action-extension-click-me](../../images/user-action-extension-click-me.png) + +You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can access to the current entity in your code. + +## How to Set Up + +In this example, we will add a "Click Me!" action and execute a JavaScript code for the user management page of the [Identity Module](../../Modules/Identity.md). + +### Create a JavaScript File + +First, add a new JavaScript file to your solution. We added inside the `/Pages/Identity/Users` folder of the `.Web` project: + +![user-action-extension-on-solution](../../images/user-action-extension-on-solution.png) + +Here, the content of this JavaScript file: + +```js +var clickMeAction = { + text: 'Click Me!', + action: function(data) { + //TODO: Write your custom code + alert(data.record.userName); + } +}; + +abp.ui.extensions.entityActions + .get('identity.user') + .addContributor(function(actionList) { + actionList.addTail(clickMeAction); + }); +``` + +In the `action` function, you can do anything you need. See the API section for a detailed usage. + +### Add the File to the User Management Page + +Then you need to add this JavaScript file to the user management page. You can take the power of the [Bundling & Minification System](Bundling-Minification.md). + +Write the following code inside the `ConfigureServices` of your module class: + +```csharp +Configure(options => +{ + options.ScriptBundles.Configure( + typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName, + bundleConfiguration => + { + bundleConfiguration.AddFiles( + "/Pages/Identity/Users/my-user-extensions.js" + ); + }); +}); +``` + +This configuration adds `my-user-extensions.js` to the user management page of the Identity Module. `typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName` is the name of the bundle in the user management page. This is a common convention used for all the ABP Commercial modules. + +That's all. Run your application to see the result. + +## API + +This section explains details of the `abp.ui.extensions.entityActions` JavaScript API. + +### abp.ui.extensions.entityActions.get(entityName) + +This method is used to access the entity actions of a specific module. It takes one parameter: + +* **entityName**: The name of the entity defined by the related module. + +### abp.ui.extensions.entityActions.get(entityName).actions + +The `actions` property is used to retrieve a [doubly linked list](../Common/Utils/Linked-List.md) of previously defined actions for an entity. All contributors are executed in order to prepare the final actions list. This is normally called by the modules to show the actions in the grid. However, you can use it if you are building your own extensible UIs. + +### abp.ui.extensions.entityActions.get(entityName).addContributor(contributeCallback) + +The `addContributor` method covers all scenarios, e.g. you want to add your action in a different position in the list, change or remove an existing action item. `addContributor` with the following parameter: + +* **contributeCallback**: A callback function that is called whenever the action list should be created. You can freely modify the action list inside this callback method. + +#### Example + +```js +var clickMe2Action = { + text: 'Click Me 2!', + icon: 'fas fa-hand-point-right', + action: function(data) { + //TODO: Write your custom code + alert(data.record.userName); + } +}; + +abp.ui.extensions.entityActions + .get('identity.user') + .addContributor(function(actionList) { + // Remove an item from actionList + actionList.dropHead(); + + // Add the new item to the actionList + actionList.addHead(clickMe2Action); + }); +``` + +> `actionList` is [linked list](../Common/Utils/Linked-List.md). You can use its methods to build a list of columns however you need. diff --git a/docs/en/UI/AspNetCore/Layout-Hooks.md b/docs/en/UI/AspNetCore/Layout-Hooks.md index 7039749142..168454a7c2 100644 --- a/docs/en/UI/AspNetCore/Layout-Hooks.md +++ b/docs/en/UI/AspNetCore/Layout-Hooks.md @@ -1,4 +1,4 @@ -# ASP.NET Core MVC / Razor Pages Layout Hooks +# ASP.NET Core MVC / Razor Pages: Layout Hooks ABP Framework theming system places the page layout into the [theme](Theming.md) NuGet packages. That means the final application doesn't include a `Layout.cshtml`, so you can't directly change the layout code to customize it. diff --git a/docs/en/UI/AspNetCore/Libraries/DatatablesNet.md b/docs/en/UI/AspNetCore/Libraries/DatatablesNet.md deleted file mode 100644 index a5a66e9e19..0000000000 --- a/docs/en/UI/AspNetCore/Libraries/DatatablesNet.md +++ /dev/null @@ -1,3 +0,0 @@ -# ABP Datatables.Net Integration for ASP.NET Core UI - -TODO \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Navigation-Menu.md b/docs/en/UI/AspNetCore/Navigation-Menu.md index ddbfbbd857..7a8fe31e3e 100644 --- a/docs/en/UI/AspNetCore/Navigation-Menu.md +++ b/docs/en/UI/AspNetCore/Navigation-Menu.md @@ -13,6 +13,8 @@ So, ABP Framework **provides a menu infrastructure** where; In order to add menu items (or manipulate the existing items) you need to create a class implementing the `IMenuContributor` interface. +> The [application startup template](../../Startup-Templates/Application.md) already contains an implementation of the `IMenuContributor`. So, you can add items inside that class instead of creating a new one. + **Example: Add a *CRM* menu item with *Customers* and *Orders* sub menu items** ```csharp @@ -66,6 +68,16 @@ Configure(options => }); ```` +This example uses some localization keys as display names those should be defined in the localization file: + +````json +"Menu:CRM": "CRM", +"Menu:Orders": "Orders", +"Menu:Customers": "Customers" +```` + +See the [localization document](../../Localization.md) to learn more about the localization. + When you run the application, you will see the menu items added to the main menu: ![nav-main-menu](../../images/nav-main-menu.png) @@ -83,13 +95,13 @@ Here, a few notes on the menu contributors; There are more options of a menu item (the constructor of the `ApplicationMenuItem` class). Here, the list of all available options; -* `name` (`string`, required): The unique name of the menu item. +* `name` (`string`, required): The **unique name** of the menu item. * `displayName` (`string`, required): Display name/text of the menu item. You can [localize](../../Localization.md) this as shown before. * `url` (`string`): The URL of the menu item. * `icon` (`string`): An icon name. Free [Font Awesome](https://fontawesome.com/) icon classes are supported out of the box. Example: `fa fa-book`. You can use any CSS font icon class as long as you include the necessary CSS files to your application. * `order` (`int`): The order of the menu item. Default value is `1000`. Items are sorted by the adding order unless you specify an order value. * `customData` (`object`): A custom object that you can associate to the menu item and use it while rendering the menu item. -* `target` (`string`): Target of the menu item. Can be `null` (default), "_blank", "_*self*", "_parent", "_*top*" or a frame name for web applications. +* `target` (`string`): Target of the menu item. Can be `null` (default), "\_*blank*", "\_*self*", "\_*parent*", "\_*top*" or a frame name for web applications. * `elementId` (`string`): Can be used to render the element with a specific HTML `id` attribute. * `cssClass` (`string`): Additional string classes for the menu item. @@ -97,7 +109,7 @@ There are more options of a menu item (the constructor of the `ApplicationMenuIt As seen above, a menu contributor contributes to the menu dynamically. So, you can perform any custom logic or get menu items from any source. -One use case is the [authorization](Authorization.md). You typically want to add menu items by checking a permission. +One use case is the [authorization](../../Authorization.md). You typically want to add menu items by checking a permission. **Example: Check if the current user has a permission** diff --git a/docs/en/UI/AspNetCore/Overall.md b/docs/en/UI/AspNetCore/Overall.md new file mode 100644 index 0000000000..8cd9dd3f5e --- /dev/null +++ b/docs/en/UI/AspNetCore/Overall.md @@ -0,0 +1,159 @@ +# ASP.NET Core MVC / Razor Pages UI + +## Introduction + +ABP Framework provides a convenient and comfortable way of creating web applications using the ASP.NET Core MVC / Razor Pages as the User Interface framework. + +> ABP doesn't offer a new/custom way of UI development. You can continue to use your current skills to create the UI. However, it offers a lot of features to make your development easier and have a more maintainable code base. + +### MVC vs Razor Pages + +ASP.NET Core provides two models for UI development: + +* **[MVC (Model-View-Controller)](https://docs.microsoft.com/en-us/aspnet/core/mvc/)** is the classic way that exists from the version 1.0. This model can be used to create UI pages/components and HTTP APIs. +* **[Razor Pages](https://docs.microsoft.com/en-us/aspnet/core/razor-pages/)** was introduced with the ASP.NET Core 2.0 as a new way to create web pages. + +**ABP Framework supports both** of the MVC and the Razor Pages models. However, it is suggested to create the **UI pages with Razor Pages** approach and use the **MVC model to build HTTP APIs**. So, all the pre-build modules, samples and the documentation is based on the Razor Pages for the UI development, while you can always apply the MVC pattern to create your own pages. + +### Modularity + +[Modularity](../../Module-Development-Basics.md) is one of the key goals of the ABP Framework. It is not different for the UI; It is possible to develop modular applications and reusable application modules with isolated and reusable UI pages and components. + +The [application startup template](../../Startup-Templates/Application.md) comes with some application modules pre-installed. These modules have their own UI pages embedded into their own NuGet packages. You don't see their code in your solution, but they work as expected on runtime. + +## Theme System + +ABP Framework provides a complete [Theming](Theming.md) system with the following goals: + +* Reusable [application modules](../../Modules/Index.md) are developed **theme-independent**, so they can work with any UI theme. +* UI theme is **decided by the final application**. +* The theme is distributed via NuGet/NPM packages, so it is **easily upgradable**. +* The final application can **customize** the selected theme. + +### Current Themes + +Currently, two themes are **officially provided**: + +* The [Basic Theme](Basic-Theme.md) is the minimalist theme with the plain Bootstrap style. It is **open source and free**. +* The [Lepton Theme](https://commercial.abp.io/themes) is a **commercial** theme developed by the core ABP team and is a part of the [ABP Commercial](https://commercial.abp.io/) license. + +There are also some community-driven themes for the ABP Framework (you can search on the web). + +### Base Libraries + +There are a set of standard JavaScript/CSS libraries that comes pre-installed and supported by all the themes: + +- [Twitter Bootstrap](https://getbootstrap.com/) as the fundamental HTML/CSS framework. +- [JQuery](https://jquery.com/) for DOM manipulation. +- [DataTables.Net](https://datatables.net/) for data grids. +- [JQuery Validation](https://jqueryvalidation.org/) for client side & [unobtrusive](https://github.com/aspnet/jquery-validation-unobtrusive) validation +- [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. +- [SweetAlert](https://sweetalert.js.org/) to show fancy alert message and confirmation dialogs. +- [Toastr](https://github.com/CodeSeven/toastr) to show toast notifications. +- [Lodash](https://lodash.com/) as a utility library. +- [Luxon](https://moment.github.io/luxon/) for date/time operations. +- [JQuery Form](https://github.com/jquery-form/form) for AJAX forms. +- [bootstrap-datepicker](https://github.com/uxsolutions/bootstrap-datepicker) to show date pickers. +- [Select2](https://select2.org/) for better select/combo boxes. +- [Timeago](http://timeago.yarp.com/) to show automatically updating fuzzy timestamps. +- [malihu-custom-scrollbar-plugin](https://github.com/malihu/malihu-custom-scrollbar-plugin) for custom scrollbars. + +You can use these libraries directly in your applications, without needing to manually import your page. + +### Layouts + +The themes provide the standard layouts. So, you have responsive layouts with the standard features already implemented. The screenshot below has taken from the Application Layout of the [Basic Theme](Basic-Theme.md): + +![basic-theme-application-layout](../../images/basic-theme-application-layout.png) + +See the [Theming](Theming.md) document for more layout options and other details. + +### Layout Parts + +A typical layout consists of multiple parts. The [Theming](Theming.md) system provides [menus](Navigation-Menu.md), [toolbars](Toolbars.md), [layout hooks](Layout-Hooks.md) and more to dynamically control the layout by your application and the modules you are using. + +## Features + +This section highlights some of the features provided by the ABP Framework for the ASP.NET Core MVC / Razor Pages UI. + +### Dynamic JavaScript API Client Proxies + +Dynamic JavaScript API Client Proxy system allows you to consume your server side HTTP APIs from your JavaScript client code, just like calling local functions. + +**Example: Get a list of authors from the server** + +````js +acme.bookStore.authors.author.getList({ + maxResultCount: 10 +}).then(function(result){ + console.log(result.items); +}); +```` + +`acme.bookStore.authors.author.getList` is an auto-generated function that internally makes an AJAX call to the server. + +See the [Dynamic JavaScript API Client Proxies](Dynamic-JavaScript-Proxies.md) document for more. + +### Bootstrap Tag Helpers + +ABP makes it easier & type safe to write Bootstrap HTML. + +**Example: Render a Bootstrap modal** + +````html + + + + Woohoo, you're reading this text in a modal! + + + +```` + +See the [Tag Helpers](Tag-Helpers/Index.md) document for more. + +### Forms & Validation + +ABP provides `abp-dynamic-form` and `abp-input` tag helpers to dramatically simplify to create a fully functional form that automates localization, validation and AJAX submission. + +**Example: Use `abp-dynamic-form` to create a complete form based on a model** + +````html + +```` + +See the [Forms & Validation](Forms-Validation.md) document for details. + +### Bundling & Minification / Client Side Libraries + +ABP provides a flexible and modular Bundling & Minification system to create bundles and minify style/script files on runtime. + +````html + + + + + + +```` + +Also, Client Side Package Management system offers a modular and consistent way of managing 3rd-party library dependencies. + +See the [Bundling & Minification](Bundling-Minification.md) and [Client Side Package Management](Client-Side-Package-Management.md) documents. + +### JavaScript APIs + +[JavaScript APIs](JavaScript-API/Index.md) provides a strong abstractions to the server side localization, settings, permissions, features... etc. They also provide a simple way to show messages and **notifications** to the user. + +### Modals, Alerts, Widgets and More + +ABP Framework provides a lot of built-in solutions to common application requirements; + +* [Widget System](Widgets.md) can be used to create reusable widgets & create dashboards. +* [Page Alerts](Page-Alerts.md) makes it easy to show alerts to the user. +* [Modal Manager](Modals.md) provides a simple way to build and use modals. +* [Data Tables](Data-Tables.md) integration makes straightforward to create data grids. + +## Customization + +There are a lot of ways to customize the theme and the UIs of the pre-built modules. You can override components, pages, static resources, bundles and more. See the [User Interface Customization Guide](Customization-User-Interface.md). diff --git a/docs/en/UI/AspNetCore/Page-Header.md b/docs/en/UI/AspNetCore/Page-Header.md new file mode 100644 index 0000000000..f6ec3074de --- /dev/null +++ b/docs/en/UI/AspNetCore/Page-Header.md @@ -0,0 +1,58 @@ +# ASP.NET Core MVC / Razor Pages: Page Header + +`IPageLayout` service can be used to set the page title, selected menu item and the breadcrumb items for a page. It's the [theme](Theming.md)'s responsibility to render these on the page. + +## IPageLayout + +`IPageLayout` can be injected in any page/view to set the page header properties. + +### Page Title + +Page Title can be set as shown in the example below: + +````csharp +@inject IPageLayout PageLayout +@{ + PageLayout.Content.Title = "Book List"; +} +```` + +* The Page Title is set to the HTML `title` tag (in addition to the [brand/application name](Branding.md)). +* The theme may render the Page Title before the Page Content (not implemented by the Basic Theme). + +### Breadcrumb + +> **The [Basic Theme](Basic-Theme.md) currently doesn't implement the breadcrumbs.** + +Breadcrumb items can be added to the `PageLayout.Content.BreadCrumb`. + +**Example: Add Language Management to the breadcrumb items.** + +```` +PageLayout.Content.BreadCrumb.Add("Language Management"); +```` + +The theme then renders the breadcrumb. An example render result can be: + +![breadcrumbs-example](../../images/breadcrumbs-example.png) + +* The Home icon is rendered by default. Set `PageLayout.Content.BreadCrumb.ShowHome` to `false` to hide it. +* Current Page name (got from the `PageLayout.Content.Title`) is added as the last by default. Set `PageLayout.Content.BreadCrumb.ShowCurrent` to `false` to hide it. + +Any item that you add is inserted between Home and Current Page items. You can add as many item as you need. `BreadCrumb.Add(...)` method gets three parameters: + +* `text`: The text to show for the breadcrumb item. +* `url` (optional): A URL to navigate to, if the user clicks to the breadcrumb item. +* `icon` (optional): An icon class (like `fas fa-user-tie` for Font-Awesome) to show with the `text`. + +### The Selected Menu Item + +> **The [Basic Theme](Basic-Theme.md) currently doesn't implement the selected menu item since it is not applicable to the top menu which is the only option for the Basic Theme for now.** + +You can set the Menu Item name related to this page: + +````csharp +PageLayout.Content.MenuItemName = "BookStore.Books"; +```` + +Menu item name should match a unique menu item name defined using the [Navigation / Menu](Navigation-Menu.md) system. In this case, it is expected from the theme to make the menu item "active" in the main menu. \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Page-Toolbar-Extensions.md b/docs/en/UI/AspNetCore/Page-Toolbar-Extensions.md new file mode 100644 index 0000000000..db466bebdb --- /dev/null +++ b/docs/en/UI/AspNetCore/Page-Toolbar-Extensions.md @@ -0,0 +1,163 @@ +# Page Toolbar Extensions for ASP.NET Core UI + +Page toolbar system allows you to add components to the toolbar of any page. The page toolbar is the area right to the header of a page. A button ("Import users from excel") was added to the user management page below: + +![page-toolbar-button](../../images/page-toolbar-button.png) + +You can add any type of view component item to the page toolbar or modify existing items. + +## How to Set Up + +In this example, we will add an "Import users from excel" button and execute a JavaScript code for the user management page of the [Identity Module](../../Modules/Identity.md). + +### Add a New Button to the User Management Page + +Write the following code inside the `ConfigureServices` of your web module class: + +````csharp +Configure(options => +{ + options.Configure(toolbar => + { + toolbar.AddButton( + LocalizableString.Create("ImportFromExcel"), + icon: "file-import", + id: "ImportUsersFromExcel", + type: AbpButtonType.Secondary + ); + }); +}); +```` + +`AddButton` is a shortcut to simply add a button component. Note that you need to add the `ImportFromExcel` to your localization dictionary (json file) to localize the text. + +When you run the application, you will see the button added next to the current button list. There are some other parameters of the `AddButton` method (for example, use `order` to set the order of the button component relative to the other components). + +### Create a JavaScript File + +Now, we can go to the client side to handle click event of the new button. First, add a new JavaScript file to your solution. We added inside the `/Pages/Identity/Users` folder of the `.Web` project: + +![user-action-extension-on-solution](../../images/user-action-extension-on-solution.png) + +Here, the content of this JavaScript file: + +````js +$(function () { + $('#ImportUsersFromExcel').click(function (e) { + e.preventDefault(); + alert('TODO: import users from excel'); + }); +}); +```` + +In the `click` event, you can do anything you need to do. + +### Add the File to the User Management Page + +Then you need to add this JavaScript file to the user management page. You can take the power of the [Bundling & Minification system](Bundling-Minification.md). + +Write the following code inside the `ConfigureServices` of your module class: + +````csharp +Configure(options => +{ + options.ScriptBundles.Configure( + typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName, + bundleConfiguration => + { + bundleConfiguration.AddFiles( + "/Pages/Identity/Users/my-user-extensions.js" + ); + }); +}); +```` + +This configuration adds `my-user-extensions.js` to the user management page of the Identity Module. `typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName` is the name of the bundle in the user management page. This is a common convention used for all the ABP Commercial modules. + +## Advanced Use Cases + +While you typically want to add a button action to the page toolbar, it is possible to add any type of component. + +### Add View Component to a Page Toolbar + +First, create a new view component in your project: + +![page-toolbar-custom-component](../../images/page-toolbar-custom-component.png) + +For this example, we've created a `MyToolbarItem` view component under the `/Pages/Identity/Users/MyToolbarItem` folder. + +`MyToolbarItemViewComponent.cs` content: + +````csharp +public class MyToolbarItemViewComponent : AbpViewComponent +{ + public IViewComponentResult Invoke() + { + return View("~/Pages/Identity/Users/MyToolbarItem/Default.cshtml"); + } +} +```` + +`Default.cshtml` content: + +````xml + + + +```` + +* `.cshtml` file can contain any type of component(s). It is a typical view component. +* `MyToolbarItemViewComponent` can inject and use any service if you need. + +Then you can add the `MyToolbarItemViewComponent` to the user management page: + +````csharp +Configure(options => +{ + options.Configure( + toolbar => + { + toolbar.AddComponent(); + } + ); +}); +```` + +* If your component accepts arguments (in the `Invoke`/`InvokeAsync` method), you can pass them to the `AddComponent` method as an anonymous object. + +#### Permissions + +If your button/component should be available based on a [permission/policy](../../Authorization.md), you can pass the permission/policy name as the `requiredPolicyName` parameter to the `AddButton` and `AddComponent` methods. + +### Add a Page Toolbar Contributor + +If you perform advanced custom logic while adding an item to a page toolbar, you can create a class that implements the `IPageToolbarContributor` interface or inherits from the `PageToolbarContributor` class: + +````csharp +public class MyToolbarContributor : PageToolbarContributor +{ + public override Task ContributeAsync(PageToolbarContributionContext context) + { + context.Items.Insert(0, new PageToolbarItem(typeof(MyToolbarItemViewComponent))); + + return Task.CompletedTask; + } +} +```` + +* You can use `context.ServiceProvider` to resolve dependencies if you need. + +Then add your class to the `Contributors` list: + +````csharp +Configure(options => +{ + options.Configure( + toolbar => + { + toolbar.Contributors.Add(new MyToolbarContributor()); + } + ); +}); +```` + diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Index.md b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md index 54c37af688..5b16d9ccdc 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Index.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md @@ -13,10 +13,10 @@ ABP Framework also adds some **useful features** to the standard bootstrap compo Here, the list of components those are wrapped by the ABP Framework: * [Alerts](Alerts.md) -* [Badges](Badges.md)) +* [Badges](Badges.md) * [Blockquote](Blockquote.md) * [Borders](Borders.md) -* [Breadcrumb](Breadcrumb.md) +* [Breadcrumb](Breadcrumbs.md) * [Buttons](Buttons.md) * [Cards](Cards.md) * [Carousel](Carousel.md) @@ -42,4 +42,4 @@ Here, the list of components those are wrapped by the ABP Framework: ## Dynamic Forms -**Abp Tag helpers** offer an easy way to build complete **Bootstrap forms**. See [Dynamic Forms documentation](Dynamic-Forms.md). \ No newline at end of file +**Abp Tag helpers** offer an easy way to build complete **Bootstrap forms**. See [Dynamic Forms documentation](Dynamic-Forms.md). diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Modals.md b/docs/en/UI/AspNetCore/Tag-Helpers/Modals.md index c811dee0bc..508d434671 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Modals.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Modals.md @@ -11,7 +11,7 @@ Basic usage: ````xml Launch modal - + Woohoo, you're reading this text in a modal! @@ -33,6 +33,13 @@ A value indicates the positioning of the modal. Should be one of the following v * `false` (default value) * `true` +### Scrollable + +A value indicates the scrolling of the modal. Should be one of the following values: + +* `false` (default value) +* `true` + ### size A value indicates the size of the modal. Should be one of the following values: @@ -78,4 +85,4 @@ A value indicates the positioning of your modal footer buttons. Should be one of * `Center` * `Around` * `Between` -* `End` \ No newline at end of file +* `End` diff --git a/docs/en/UI/AspNetCore/Testing.md b/docs/en/UI/AspNetCore/Testing.md new file mode 100644 index 0000000000..2c880594d3 --- /dev/null +++ b/docs/en/UI/AspNetCore/Testing.md @@ -0,0 +1,220 @@ +# ASP.NET Core MVC / Razor Pages: Testing + +> You can follow the [ASP.NET Core Integration Tests documentation](https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests) to learn details of ASP.NET Core integration tests. This document explains the additional test infrastructure provided by the ABP Framework. + +## The Application Startup Template + +The Application Startup Template contains the `.Web` project that contains UI views/pages/components of the application and a `.Web.Tests` project to test these. + +![aspnetcore-web-tests-in-solution](../../images/aspnetcore-web-tests-in-solution.png) + +## Testing the Razor Pages + +Assume that you've created a Razor Page, named `Issues.cshtml` with the following contents; + +**Issues.cshtml.cs** + +````csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; +using MyProject.Issues; + +namespace MyProject.Web.Pages +{ + public class IssuesModel : PageModel + { + public List Issues { get; set; } + + private readonly IIssueAppService _issueAppService; + + public IssuesModel(IIssueAppService issueAppService) + { + _issueAppService = issueAppService; + } + + public async Task OnGetAsync() + { + Issues = await _issueAppService.GetListAsync(); + } + } +} +```` + +**Issues.cshtml** + +````html +@page +@model MyProject.Web.Pages.IssuesModel +

Issue List

+ + + + + + + + + @foreach (var issue in Model.Issues) + { + + + + + } + +
IssueClosed?
@issue.Title + @if (issue.IsClosed) + { + Closed + } + else + { + Open + } +
+```` + +This page simply creates a table with the issues: + +![issue-list](../../images/issue-list.png) + +You can write a test class inside the `.Web.Tests` project just like the example below: + +````csharp +using System.Threading.Tasks; +using HtmlAgilityPack; +using Shouldly; +using Xunit; + +namespace MyProject.Pages +{ + public class Issues_Tests : MyProjectWebTestBase + { + [Fact] + public async Task Should_Get_Table_Of_Issues() + { + // Act + + var response = await GetResponseAsStringAsync("/Issues"); + + //Assert + + var htmlDocument = new HtmlDocument(); + htmlDocument.LoadHtml(response); + + var tableElement = htmlDocument.GetElementbyId("IssueTable"); + tableElement.ShouldNotBeNull(); + + var trNodes = tableElement.SelectNodes("//tbody/tr"); + trNodes.Count.ShouldBeGreaterThan(0); + } + } +} +```` + +`GetResponseAsStringAsync` is a shortcut method that comes from the base class that performs a HTTP GET request, checks if the resulting HTTP Status is `200` and returns the response as a `string`. + +> You can use the base `Client` object (of type `HttpClient`) to perform any kind of request to the server and read the response yourself. `GetResponseAsStringAsync` is just a shortcut method. + +This example uses the [HtmlAgilityPack](https://html-agility-pack.net/) library to parse the incoming HTML and test if it contains the issue table. + +> This example assumes there are some initial issues in the database. See the *The Data Seed* section of the [Testing document](../../Testing.md) to learn how to seed test data, so your tests can assume some initial data available in the database. + +## Testing the Controllers + +Testing a controller is not different. Just perform a request to the server with a proper URL, get the response and make your assertions. + +### View Result + +If the controller returns a View, you can use a similar code to test the returned HTML. See the Razor Pages example above. + +### Object Result + +If the controller returns an object result, you can use the `GetResponseAsObjectAsync` base method. + +Assume that you've a controller as defined below: + +````csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using MyProject.Issues; +using Volo.Abp.AspNetCore.Mvc; + +namespace MyProject.Web.Controllers +{ + [Route("api/issues")] + public class IssueController : AbpController + { + private readonly IIssueAppService _issueAppService; + + public IssueController(IIssueAppService issueAppService) + { + _issueAppService = issueAppService; + } + + [HttpGet] + public async Task> GetAsync() + { + return await _issueAppService.GetListAsync(); + } + } +} +```` + +You can write a test code to execute the API and get the result: + +````csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using MyProject.Issues; +using Shouldly; +using Xunit; + +namespace MyProject.Pages +{ + public class Issues_Tests : MyProjectWebTestBase + { + [Fact] + public async Task Should_Get_Issues_From_Api() + { + var issues = await GetResponseAsObjectAsync>("/api/issues"); + + issues.ShouldNotBeNull(); + issues.Count.ShouldBeGreaterThan(0); + } + } +} +```` + +## Testing the JavaScript Code + +ABP Framework doesn't provide any infrastructure to test your JavaScript code. You can use any test framework and tooling to test your JavaScript code. + +## The Test Infrastructure + +[Volo.Abp.AspNetCore.TestBase](https://www.nuget.org/packages/Volo.Abp.AspNetCore.TestBase) package provides the test infrastructure that is integrated to the ABP Framework and ASP.NET Core. + +> Volo.Abp.AspNetCore.TestBase package is already installed in the `.Web.Tests` project. + +This package provides the `AbpAspNetCoreIntegratedTestBase` as the fundamental base class to derive the test classes from. The `MyProjectWebTestBase` base class used above inherits from the `AbpAspNetCoreIntegratedTestBase`, so we indirectly inherited the `AbpAspNetCoreIntegratedTestBase`. + +### Base Properties + +The `AbpAspNetCoreIntegratedTestBase` provides the following base properties those are used in the tests: + +* `Server`: A `TestServer` instance that hosts the web application in tests. +* `Client`: An `HttpClient` instance that is configured to perform requests to the test server. +* `ServiceProvider`: The service provider that you can resolve services in case of need. + +### Base Methods + +`AbpAspNetCoreIntegratedTestBase` provides the following methods that you can override if you need to customize the test server: + +* `ConfigureServices` can be overridden to register/replace services only for the derived test class. +* `CreateHostBuilder` can be used to customize building the `IHostBuilder`. + +See Also + +* [Overall / Server Side Testing](../../Testing.md) \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Theming.md b/docs/en/UI/AspNetCore/Theming.md index 594a882821..85acf65433 100644 --- a/docs/en/UI/AspNetCore/Theming.md +++ b/docs/en/UI/AspNetCore/Theming.md @@ -32,7 +32,7 @@ All the themes must depend on the [@abp/aspnetcore.mvc.ui.theme.shared](https:// * [Twitter Bootstrap](https://getbootstrap.com/) as the fundamental HTML/CSS framework. * [JQuery](https://jquery.com/) for DOM manipulation. * [DataTables.Net](https://datatables.net/) for data grids. -* [JQuery Validation](https://jqueryvalidation.org/) for client side & [unobtrusive](https://github.com/aspnet/jquery-validation-unobtrusive) validation +* [JQuery Validation](https://github.com/jquery-validation/jquery-validation) for client side & [unobtrusive](https://github.com/aspnet/jquery-validation-unobtrusive) validation * [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. * [SweetAlert](https://sweetalert.js.org/) to show fancy alert message and confirmation dialogs. * [Toastr](https://github.com/CodeSeven/toastr) to show toast notifications. @@ -114,7 +114,7 @@ The [Lepton Theme](https://commercial.abp.io/themes) shows the application logo #### The Empty Layout -The empty layout provides an empty page, however it typically includes the following parts; +The empty layout provides an empty page. It typically includes the following parts; * [Page alerts](Page-Alerts.md) * The page content (aka `RenderBody()`) @@ -177,4 +177,260 @@ Configure(options => #### The IThemeSelector Service -ABP Framework allows to use multiple themes together. This is why `options.Themes` is a list. `IThemeSelector` service selects the theme on the runtime. The application developer can set the `AbpThemingOptions.DefaultThemeName` to set the theme to be used, or replace the `IThemeSelector` service implementation (the default implementation is `DefaultThemeSelector`) to completely control the theme selection on runtime. \ No newline at end of file +ABP Framework allows to use multiple themes together. This is why `options.Themes` is a list. `IThemeSelector` service selects the theme on the runtime. The application developer can set the `AbpThemingOptions.DefaultThemeName` to set the theme to be used, or replace the `IThemeSelector` service implementation (the default implementation is `DefaultThemeSelector`) to completely control the theme selection on runtime. + +### Bundles + +[Bundling system](Bundling-Minification.md) provides a standard way to import style & script files into pages. There are two standard bundles defined by the ABP Framework: + +* `StandardBundles.Styles.Global`: The global bundle that includes the style files used in all the pages. Typically, it includes the CSS files of the Base Libraries. +* `StandardBundles.Scripts.Global`: The global bundle that includes the script files used in all the pages. Typically, it includes the JavaScript files of the Base Libraries. + +A theme generally extends these standard bundles by adding theme specific CSS/JavaScript files. + +The best way to define new bundles, inherit from the standard bundles and add to the `AbpBundlingOptions` as shown below (this code is from the [Basic Theme](Basic-Theme.md)): + +````csharp +Configure(options => +{ + options + .StyleBundles + .Add(BasicThemeBundles.Styles.Global, bundle => + { + bundle + .AddBaseBundles(StandardBundles.Styles.Global) + .AddContributors(typeof(BasicThemeGlobalStyleContributor)); + }); + + options + .ScriptBundles + .Add(BasicThemeBundles.Scripts.Global, bundle => + { + bundle + .AddBaseBundles(StandardBundles.Scripts.Global) + .AddContributors(typeof(BasicThemeGlobalScriptContributor)); + }); +}); +```` + +`BasicThemeGlobalStyleContributor` and `BasicThemeGlobalScriptContributor` are bundle contributors. For example, `BasicThemeGlobalStyleContributor` is defined as shown below: + +```csharp +public class BasicThemeGlobalStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.Add("/themes/basic/layout.css"); + } +} +``` + +Then the theme can render these bundles in a layout. For example, you can render the Global Styles as shown below: + +````html + +```` + +See the [Bundle & Minification](Bundling-Minification.md) document to understand the Bundling system better. + +### Layout Parts + +A typical Layout consists of several parts. The theme should include the necessary parts in each layout. + +**Example: The Basic Theme has the following parts for the Application Layout** + +![basic-theme-application-layout-parts](../../images/basic-theme-application-layout-parts.png) + +The application code and the modules can only show contents in the Page Content part. If they need to change the other parts (to add a menu item, to add a toolbar item, to change the application name in the branding area...) they should use the ABP Framework APIs. + +The following sections explain the fundamental parts pre-defined by the ABP Framework and can be implemented by the themes. + +> It is a good practice to split the layout into components/partials, so the final application can override them partially for customization purpose. + +#### Branding + +`IBrandingProvider` service should be used to get the name and the logo URL of the application to render in the Branding part. + +The [Application Startup Template](../../Startup-Templates/Application.md) has an implementation of this interface to set the values by the application developer. + +#### Main Menu + +`IMenuManager` service is used to get the main menu items and render on the layout. + +**Example: Get the Main Menu to render in a view component** + +```csharp +public class MainNavbarMenuViewComponent : AbpViewComponent +{ + private readonly IMenuManager _menuManager; + + public MainNavbarMenuViewComponent(IMenuManager menuManager) + { + _menuManager = menuManager; + } + + public async Task InvokeAsync() + { + var menu = await _menuManager.GetAsync(StandardMenus.Main); + return View("~/Themes/Basic/Components/Menu/Default.cshtml", menu); + } +} +``` + +See the [Navigation / Menus](Navigation-Menu.md) document to learn more about the navigation system. + +#### Main Toolbar + +`IToolbarManager` service is used to get the Main Toolbar items and render on the layout. Each item of this toolbar is a View Component, so it may include any type of UI elements. Inject the `IToolbarManager` and use the `GetAsync` to get the toolbar items: + +````csharp +var toolbar = await _toolbarManager.GetAsync(StandardToolbars.Main); +```` + +> See the [Toolbars](Toolbars.md) document to learn more on the toolbar system. + +The theme has a responsibility to add two pre-defined items to the main toolbar: Language Selection and User Menu. To do that, create a class implementing the `IToolbarContributor` interface and add it to the `AbpToolbarOptions` as shown below: + +```csharp +Configure(options => +{ + options.Contributors.Add(new BasicThemeMainTopToolbarContributor()); +}); +``` + +##### Language Selection + +Language Selection toolbar item is generally a dropdown that is used to switch between languages. `ILanguageProvider` is used to get the list of available languages and `CultureInfo.CurrentUICulture` is used to learn the current language. + +`/Abp/Languages/Switch` endpoint can be used to switch the language This endpoint accepts the following query string parameters: + +* `culture`: The selected culture, like `en-US` or `en`. +* `uiCulture`: The selected UI culture, like `en-US` or `en`. +* `returnUrl` (optional): Can be used to return a given URL after switching the language. + +`culture` and `uiCulture` should match one of the available languages. ABP Framework sets a culture cookie in the `/Abp/Languages/Switch` endpoint. + +##### User Menu + +User menu includes links related to the user account. `IMenuManager` is used just like the Main Menu, but this time with `StandardMenus.User` parameter like shown below: + +````csharp +var menu = await _menuManager.GetAsync(StandardMenus.User); +```` + +[ICurrentUser](../../CurrentUser.md) and [ICurrentTenant](../../Multi-Tenancy.md) services can be used to obtain the current user and tenant names. + +#### Page Alerts + +`IAlertManager` service is used to get the current page alerts to render on the layout. Use the `Alerts` list of the `IAlertManager`. It is generally rendered just before the page content (`RenderBody()`). + +See the [Page Alerts](Page-Alerts.md) document to learn more. + +#### Layout Hooks + +Since the Layout is in the theme package, the final application or any module can't directly manipulate the layout content. The [Layout Hook](Layout-Hooks.md) system allows to inject components to some specific points of the layout. + +The theme is responsible to render the hooks in the correct place. + +**Example: Render the `LayoutHooks.Head.First` Hook in the Application Layout** + +````html + + @await Component.InvokeLayoutHookAsync(LayoutHooks.Head.First, StandardLayouts.Application) + ... +```` + +See the [Layout Hook](Layout-Hooks.md) document to learn the standard layout hooks. + +#### Script / Style Sections + +Every layout should render the following optional sections: + +* `styles` section is rendered in the end of the `head`, just before the `LayoutHooks.Head.Last`. +* `scripts` section is rendered in the end of the `body`, just before the `LayoutHooks.Body.Last`. + +In this way, the page can import styles and scripts to the layout. + +**Example: Render the `styles` section** + +````csharp +@await RenderSectionAsync("styles", required: false) +```` + +#### Content Toolbar Section + +Another pre-defined section is the Content Toolbar section which can be used by the pages to add code just before the page content. The Basic Theme renders it as shown below: + +````html +
+
+ @RenderSection("content_toolbar", false) +
+
+```` + +The container div's id must be `AbpContentToolbar`. This section should come before the `RenderBody()`. + +#### Widget Resources + +The [Widget System](Widgets.md) allows to define reusable widgets with their own style/script files. All the layouts should render the widget style and scripts. + +**Widget Styles** is rendered as shown below, just before the `styles` section, after the global style bundle: + +````csharp +@await Component.InvokeAsync(typeof(WidgetStylesViewComponent)) +```` + +**Widget Scripts** is rendered as shown below, just before the `scripts` section, after the global script bundle: + +````csharp +@await Component.InvokeAsync(typeof(WidgetScriptsViewComponent)) +```` + +#### ABP Scripts + +ABP has some special scripts those should be included into every layout. They are not included in the global bundles since they are dynamically created based on the current user. + +ABP scripts (`ApplicationConfigurationScript` and `ServiceProxyScript`) should be added just after the global script bundle, as shown below: + +````html + + +```` + +#### Page Title, Selected Menu Item and Breadcrumbs + +`IPageLayout` service can be injected by any page to set the Page Title, the selected menu item name and the breadcrumb items. Then the theme can use this service to get these values and render on the UI. + +The Basic Theme doesn't implement this service, but the Lepton Theme implements: + +![breadcrumbs-example](../../images/breadcrumbs-example.png) + +See the [Page Header](Page-Header.md) document for more. + +#### Tenant Switch + +The Account Layout should allow the user to switch the current tenant if the application is multi-tenant and the tenant was resolved from the cookies. See the [Basic Theme Account Layout](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml) as an example implementation. + +### Layout Classes + +The Standard Layouts (`Application`, `Account` and `Empty`) should add the following CSS classes to the `body` tag: + +* `abp-application-layout` for the `Application` layout. +* `abp-account-layout` for the `Account` layout. +* `abp-empty-layout` for the `Empty` layout. + +In this way, applications or modules can have selectors based on the current layout. + +### RTL + +To support Right-To-Left languages, the Layout should check the current culture and add `dir="rtl"` to the `html` tag and `rtl` CSS class the the `body` tag. + +You can check `CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft` to understand if the current language is a RTL language. + +### The NPM Package + +A theme should have a NPM package that depends on the [@abp/aspnetcore.mvc.ui.theme.shared](https://www.npmjs.com/package/@abp/aspnetcore.mvc.ui.theme.shared) package. In this way, it inherits all the Base Libraries. If the theme requires additional libraries, then it should define these dependencies too. + +Applications use the [Client Side Package Management](Client-Side-Package-Management.md) system to add client side libraries to the project. So, if an application uses your theme, it should add dependency to your theme's NPM package as well as the NuGet package dependency. + diff --git a/docs/en/UI/AspNetCore/Toolbars.md b/docs/en/UI/AspNetCore/Toolbars.md index cc3bcd95be..3f1185f61b 100644 --- a/docs/en/UI/AspNetCore/Toolbars.md +++ b/docs/en/UI/AspNetCore/Toolbars.md @@ -1,3 +1,74 @@ -# Toolbars +# ASP.NET Core MVC / Razor Pages UI: Toolbars -TODO \ No newline at end of file +The Toolbar system is used to define **toolbars** on the user interface. Modules (or your application) can add **items** to a toolbar, then the [theme](Theming.md) renders the toolbar on the **layout**. + +There is only one **standard toolbar** named "Main" (defined as a constant: `StandardToolbars.Main`). The [Basic Theme](Basic-Theme) renders the main toolbar as shown below: + +![bookstore-toolbar-highlighted](../../images/bookstore-toolbar-highlighted.png) + +In the screenshot above, there are two items added to the main toolbar: Language switch component & user menu. You can add your own items here. + +## Example: Add a Notification Icon + +In this example, we will add a **notification (bell) icon** to the left of the language switch item. A item in the toolbar should be a **view component**. So, first, create a new view component in your project: + +![bookstore-notification-view-component](../../images/bookstore-notification-view-component.png) + +**NotificationViewComponent.cs** + +````csharp +public class NotificationViewComponent : AbpViewComponent +{ + public async Task InvokeAsync() + { + return View("/Pages/Shared/Components/Notification/Default.cshtml"); + } +} +```` + +**Default.cshtml** + +````xml +
+ +
+```` + +Now, we can create a class implementing the `IToolbarContributor` interface: + +````csharp +public class MyToolbarContributor : IToolbarContributor +{ + public Task ConfigureToolbarAsync(IToolbarConfigurationContext context) + { + if (context.Toolbar.Name == StandardToolbars.Main) + { + context.Toolbar.Items + .Insert(0, new ToolbarItem(typeof(NotificationViewComponent))); + } + + return Task.CompletedTask; + } +} +```` + +This class adds the `NotificationViewComponent` as the first item in the `Main` toolbar. + +Finally, you need to add this contributor to the `AbpToolbarOptions`, in the `ConfigureServices` of your [module](../../Module-Development-Basics.md): + +````csharp +Configure(options => +{ + options.Contributors.Add(new MyToolbarContributor()); +}); +```` + +That's all, you will see the notification icon on the toolbar when you run the application: + +![bookstore-notification-icon-on-toolbar](../../images/bookstore-notification-icon-on-toolbar.png) + +`NotificationViewComponent` in this sample simply returns a view without any data. In real life, you probably want to **query database** (or call an HTTP API) to get notifications and pass to the view. If you need, you can add a `JavaScript` or `CSS` file to the global [bundle](Bundling-Minification.md) for your toolbar item. + +## IToolbarManager + +`IToolbarManager` is used to render the toolbar. It returns the toolbar items by a toolbar name. This is generally used by the [themes](Theming.md) to render the toolbar on the layout. \ No newline at end of file diff --git a/docs/en/UI/Blazor/Authentication.md b/docs/en/UI/Blazor/Authentication.md new file mode 100644 index 0000000000..9dc34ebce5 --- /dev/null +++ b/docs/en/UI/Blazor/Authentication.md @@ -0,0 +1,11 @@ +# Blazor UI: Authentication + +The [application startup template](../../Startup-Templates/Application.md) is properly configured to use OpenId Connect to authenticate the user through the server side login form; + +* When the Blazor application needs to authenticate, it is redirected to the server side. +* Users can enter username & password to login if they already have an account. If not, they can use the register form to create a new user. They can also use forgot password and other features. The server side uses IdentityServer4 to handle the authentication. +* Finally, they are redirected back to the Blazor application to complete the login process. + +This is a typical and recommended approach to implement authentication in Single-Page Applications. The client side configuration is done in the startup template, so you can change it. + +See the [Blazor Security document](https://docs.microsoft.com/en-us/aspnet/core/blazor/security) to understand and customize the authentication process. \ No newline at end of file diff --git a/docs/en/UI/Blazor/Authorization.md b/docs/en/UI/Blazor/Authorization.md new file mode 100644 index 0000000000..7325c8e57b --- /dev/null +++ b/docs/en/UI/Blazor/Authorization.md @@ -0,0 +1,75 @@ +# Blazor UI: Authorization + +Blazor applications can use the same authorization system and permissions defined in the server side. + +> This document is only for authorizing on the Blazor UI. See the [Server Side Authorization](../../Authorization.md) to learn how to define permissions and control the authorization system. + +## Basic Usage + +> ABP Framework is **100% compatible** with the Authorization infrastructure provided by the Blazor. See the [Blazor Security Document](https://docs.microsoft.com/en-us/aspnet/core/blazor/security/) to learn all authorization options. This section **only shows some common scenarios**. + +### Authorize Attribute + +`[Authorize]` attribute can be used to show a page only to the authenticated users. + +````csharp +@page "/" +@attribute [Authorize] + +You can only see this if you're signed in. +```` + +The `[Authorize]` attribute also supports role-based or policy-based authorization. For example, you can check permissions defined in the server side: + +````csharp +@page "/" +@attribute [Authorize("MyPermission")] + +You can only see this if you have the necessary permission. +```` + +### AuthorizeView + +`AuthorizeView` component can be used in a page/component to conditionally render a part of the content: + +````html + +

You can only see this if you satisfy the "MyPermission" policy.

+
+```` + +### IAuthorizationService + +`IAuthorizationService` can be injected and used to programmatically check permissions: + +````csharp +public partial class Index +{ + protected override async Task OnInitializedAsync() + { + if (await AuthorizationService.IsGrantedAsync("MyPermission")) + { + //... + } + } +} +```` + +If your component directly or indirectly inherits from the `AbpComponentBase`, `AuthorizationService` becomes pre-injected and ready to use. If not, you can always [inject](../../Dependency-Injection.md) the `IAuthorizationService` yourself. + +`IAuthorizationService` can also be used in the view side where `AuthorizeView` component is not enough. + +There are some useful extension methods for the `IAuthorizationService`: + +* `IsGrantedAsync` simply returns `true` or `false` for the given policy/permission. +* `CheckAsync` checks and throws `AbpAuthorizationException` if given policy/permission hasn't granted. You don't have to handle these kind of exceptions since ABP Framework automatically [handles errors](Error-Handling.md). +* `AuthorizeAsync` returns `AuthorizationResult` as the standard way provided by the ASP.NET Core authorization system. + +> See the [Blazor Security Document](https://docs.microsoft.com/en-us/aspnet/core/blazor/security/) to learn all authorization options + +## See Also + +* [Authorization](../../Authorization.md) (server side) +* [Blazor Security](https://docs.microsoft.com/en-us/aspnet/core/blazor/security/) (Microsoft documentation) +* [ICurrentUser Service](CurrentUser.md) + diff --git a/docs/en/UI/Blazor/Basic-Theme.md b/docs/en/UI/Blazor/Basic-Theme.md new file mode 100644 index 0000000000..926d15cf11 --- /dev/null +++ b/docs/en/UI/Blazor/Basic-Theme.md @@ -0,0 +1,57 @@ +# Blazor UI: Basic Theme + +The Basic Theme is a theme implementation for the Blazor UI. It is a minimalist theme that doesn't add any styling on top of the plain [Bootstrap](https://getbootstrap.com/). You can take the Basic Theme as the **base theme** and build your own theme or styling on top of it. See the *Customization* section. + +> If you are looking for a professional, enterprise ready theme, you can check the [Lepton Theme](https://commercial.abp.io/themes), which is a part of the [ABP Commercial](https://commercial.abp.io/). + +> See the [Theming document](Theming.md) to learn about themes. + +## Installation + +**This theme is already installed** when you create a new solution using the [startup templates](../../Startup-Templates/Index.md). If you need to manually install it, follow the steps below: + +* Install the [Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme) NuGet package to your web project. +* Add `AbpAspNetCoreComponentsWebAssemblyBasicThemeModule` into the `[DependsOn(...)]` attribute for your [module class](../../Module-Development-Basics.md) in the your Blazor UI project. +* Use `Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic.App` as the root component of your application in the `ConfigureServices` method of your module: + +````csharp +var builder = context.Services.GetSingletonInstance(); +builder.RootComponents.Add("#ApplicationContainer"); +```` + +`#ApplicationContainer` is a selector (like `
Loading...
`) in the `index.html`. + +## The Layout + +![basic-theme-application-layout](../../images/basic-theme-application-layout.png) + +Application Layout implements the following parts, in addition to the common parts mentioned above; + +* [Branding](Branding.md) Area +* Main [Menu](Navigation-Menu.md) +* Main [Toolbar](Toolbars.md) with Language Selection & User Menu +* [Page Alerts](Page-Alerts.md) + +## Customization + +You have two options two customize this theme: + +### Overriding Styles / Components + +In this approach, you continue to use the the theme as NuGet and NPM packages and customize the parts you need to. There are several ways to customize it; + +#### Override the Styles + +You can simply override the styles in the Global Styles file of your application. + +#### Override the Components + +See the [Customization / Overriding Components](Customization-Overriding-Components.md) to learn how you can replace components, customize and extend the user interface. + +### Copy & Customize + +You can download the [source code](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme) of the Basic Theme, copy the project content into your solution, re-arrange the package/module dependencies (see the Installation section above to understand how it was installed to the project) and freely customize the theme based on your application requirements. + +## See Also + +* [Theming](Theming.md) \ No newline at end of file diff --git a/docs/en/UI/Blazor/Branding.md b/docs/en/UI/Blazor/Branding.md new file mode 100644 index 0000000000..94bcb2bad5 --- /dev/null +++ b/docs/en/UI/Blazor/Branding.md @@ -0,0 +1,37 @@ +# Blazor UI: Branding + +## IBrandingProvider + +`IBrandingProvider` is a simple interface that is used to show the application name and logo on the layout. + +The screenshot below shows *MyProject* as the application name: + +![branding-nobrand](../../images/branding-nobrand.png) + +You can implement the `IBrandingProvider` interface or inherit from the `DefaultBrandingProvider` to set the application name: + +````csharp +using Volo.Abp.DependencyInjection; +using Volo.Abp.Ui.Branding; + +namespace MyCompanyName.MyProjectName.Blazor +{ + [Dependency(ReplaceServices = true)] + public class MyProjectNameBrandingProvider : DefaultBrandingProvider + { + public override string AppName => "Book Store"; + } +} +```` + +The result will be like shown below: + +![branding-appname](../../images/branding-appname.png) + +`IBrandingProvider` has the following properties: + +* `AppName`: The application name. +* `LogoUrl`: A URL to show the application logo. +* `LogoReverseUrl`: A URL to show the application logo on a reverse color theme (dark, for example). + +> **Tip**: `IBrandingProvider` is used in every page refresh. For a multi-tenant application, you can return a tenant specific application name to customize it per tenant. diff --git a/docs/en/UI/Blazor/Components/SubmitButton.md b/docs/en/UI/Blazor/Components/SubmitButton.md new file mode 100644 index 0000000000..1caec28c81 --- /dev/null +++ b/docs/en/UI/Blazor/Components/SubmitButton.md @@ -0,0 +1,25 @@ +# Blazor UI: SubmitButton component + +`SubmitButton` is a simple wrapper around `Button` component. It is used to be placed inside of page Form or Modal dialogs where it can response to user actions and to be activated as a default button by pressing an ENTER key. Once clicked it will go into the `disabled` state and also it will show a small loading indicator until clicked event is finished. + +## Quick Example + +```html + +``` + +Notice that we didn't specify any text, like `Save Changes`. This is because `SubmitButton` will by default pull text from the localization. If you want to change that you either specify a localization key or you can add custom content. + +### With localization key + +```html + +``` + +### With custom content + +```html + + @L["Save"] + +``` \ No newline at end of file diff --git a/docs/en/UI/Blazor/CurrentTenant.md b/docs/en/UI/Blazor/CurrentTenant.md new file mode 100644 index 0000000000..0ccee9966c --- /dev/null +++ b/docs/en/UI/Blazor/CurrentTenant.md @@ -0,0 +1,23 @@ +# Blazor UI: Current Tenant + +`ICurrentTenant` service can be used to get information about the current tenant in a [multi-tenant](../../Multi-Tenancy.md) application. `ICurrentTenant` defines the following properties; + +* `Id` (`Guid`): Id of the current tenant. Can be `null` if the current user is a host user or the tenant could not be determined. +* `Name` (`string`): Name of the current tenant. Can be `null` if the current user is a host user or the tenant could not be determined. +* `IsAvailable` (`bool`): Returns `true` if the `Id` is not `null`. + +**Example: Show the current tenant name on a page** + +````csharp +@page "/" +@using Volo.Abp.MultiTenancy +@inject ICurrentTenant CurrentTenant +@if (CurrentTenant.IsAvailable) +{ +

Current tenant name: @CurrentTenant.Name

+} +```` + +## See Also + +* [Multi-Tenancy](../../Multi-Tenancy.md) \ No newline at end of file diff --git a/docs/en/UI/Blazor/CurrentUser.md b/docs/en/UI/Blazor/CurrentUser.md new file mode 100644 index 0000000000..d0d761420a --- /dev/null +++ b/docs/en/UI/Blazor/CurrentUser.md @@ -0,0 +1,22 @@ +# Blazor UI: Current User + +`ICurrentUser` service is used to obtain information about the currently authenticated user. Inject the `ICurrentUser` into any component/page and use its properties and methods. + +**Example: Show username & email on a page** + +````csharp +@page "/" +@using Volo.Abp.Users +@inject ICurrentUser CurrentUser +@if (CurrentUser.IsAuthenticated) +{ +

Welcome @CurrentUser.UserName

+} +```` + +> If you (directly or indirectly) derived your component from the `AbpComponentBase`, you can directly use the base `CurrentUser` property. + +`ICurrentUser` provides `Id`, `Name`, `SurName`, `Email`, `Roles` and some other properties. + +> See the [Server Side Current User](../../CurrentUser) service for more information. + diff --git a/docs/en/UI/Blazor/Customization-Overriding-Components.md b/docs/en/UI/Blazor/Customization-Overriding-Components.md new file mode 100644 index 0000000000..995213f9e7 --- /dev/null +++ b/docs/en/UI/Blazor/Customization-Overriding-Components.md @@ -0,0 +1,86 @@ +# Blazor UI: Customization / Overriding Components + +This document explains how to override the user interface of a depended [application module](../../Modules/Index.md) or [theme](Theming.md) for Blazor applications. + +## Overriding a Razor Component + +The ABP Framework, pre-built themes and modules define some **re-usable razor components and pages**. These pages and components can be replaced by your application or module. + +> Since pages are just the razor components, the same principle is valid for pages too. + +### Example: Replacing the Branding Area + +The screenshot below was taken from the [Basic Theme](Basic-Theme.md) comes with the application startup template. + +![bookstore-brand-area-highlighted](../../images/bookstore-brand-area-highlighted.png) + +The [Basic Theme](Basic-Theme.md) defines some razor components for the layout. For example, the highlighted area with the red rectangle above is called *Branding* component. You probably want to customize this component by adding your **own application logo**. Let's see how to do it. + +First, create your logo and place under a folder in your web application. We used `wwwroot/bookstore-logo.png` path: + +![bookstore-logo-blazor](../../images/bookstore-logo-blazor.png) + +The next step is to create a razor component, like `MyBlazor.razor`, in your application: + +![bookstore-logo-blazor](../../images/bookstore-branding-blazor.png) + +The content of the `MyBlazor.razor` is shown below: + +````html +@using Volo.Abp.DependencyInjection +@using Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic +@inherits Branding +@attribute [ExposeServices(typeof(Branding))] +@attribute [Dependency(ReplaceServices = true)] + + + +```` + +Let's explain the code: + +* `@inherits Branding` line inherits the Branding component defined by the [Basic Theme](Basic-Theme.md) (in the `Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic` namespace). +* `@attribute [ExposeServices(typeof(Branding))]` registers this service (component) to [dependency injection](../../Dependency-Injection.md) for the `Branding` service (component). +* `@attribute [Dependency(ReplaceServices = true)]` replaces the `Branding` class (component) with this new `MyBranding` class (component). +* The rest of the code is related the content and styling of the component. + +Now, you can run the application to see the result: + +![bookstore-added-logo](../../images/bookstore-added-logo.png) + +> Since the component inherits from the component it is replacing, you can use all the non-private fields/properties/methods of the base component in the derived component. + +### Example: Replacing with the Code Behind File + +If you prefer to use code-behind file for the C# code of your component, you can use the attributes in the C# side. + +**MyBlazor.razor** + +````html +@using Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic +@inherits Branding + + + +```` + +**MyBlazor.razor.cs** + +````csharp +using Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic; +using Volo.Abp.DependencyInjection; + +namespace MyProject.Blazor.Components +{ + [ExposeServices(typeof(Branding))] + [Dependency(ReplaceServices = true)] + public partial class MyBranding + { + + } +} +```` + +## Theming + +The [Theming](Theming.md) system allows you to build your own theme. You can create your theme from scratch or get the [Basic Theme](Basic-Theme.md) and change however you like. diff --git a/docs/en/UI/Blazor/Error-Handling.md b/docs/en/UI/Blazor/Error-Handling.md new file mode 100644 index 0000000000..f43456e3a7 --- /dev/null +++ b/docs/en/UI/Blazor/Error-Handling.md @@ -0,0 +1,62 @@ +# Blazor UI: Error Handling + +Blazor, by default, shows a yellow line at the bottom of the page if any unhandled exception occurs. However, this is not useful in a real application. + +ABP provides an automatic error handling system for the Blazor UI. + +* Handles all unhandled exceptions and shows nice and useful messages to the user. +* It distinguishes different kind of exceptions. Hides internal/technical error details from the user (shows a generic error message in these cases). +* It is well integrated to the [server side exception handling](../../Exception-Handling.md) system. + +## Basic Usage + +There are different type of `Exception` classes handled differently by the ABP Framework. + +### UserFriendlyException + +`UserFriendlyException` is a special type of exception. You can directly show a error message dialog to the user by throwing such an exception. + +**Example** + +````csharp +@page "/" +@using Volo.Abp + + + +@code +{ + private void TestException() + { + throw new UserFriendlyException("A user friendly error message!"); + } +} +```` + +ABP automatically handle the exception and show an error message to the user: + +![blazor-user-friendly-exception](../../images/blazor-user-friendly-exception.png) + +> You can derive from `UserFriendlyException` or directly implement `IUserFriendlyException` interface to create your own `Exception` class if you need. + +> You can use the [localization system](Localization.md) to show localized error messages. + +### BusinessException and Other Exception Types + +See the [exception handling document](../../Exception-Handling.md) to understand different kind of Exception class and interfaces and other capabilities of the Exception Handling system. + +## Generic Errors + +If the thrown `Exception` is not a special type, it is considered as generic error and a generic error message is shown to the user: + +![blazor-generic-exception-message](../../images/blazor-generic-exception-message.png) + +> All error details (including stack trace) are still written in the browser's console. + +## Server Side Errors + +Errors (like Validation, Authorization and User Friendly Errors) sent by the server are processed as you expect and properly shown to the user. So, error handling system works end to end without need to manually handle exceptions or manually transfer server-to-client error messages. + +## See Also + +* [Exception Handling System](../../Exception-Handling.md) \ No newline at end of file diff --git a/docs/en/UI/Blazor/Global-Scripts-Styles.md b/docs/en/UI/Blazor/Global-Scripts-Styles.md new file mode 100644 index 0000000000..68628d200a --- /dev/null +++ b/docs/en/UI/Blazor/Global-Scripts-Styles.md @@ -0,0 +1,83 @@ +# Blazor UI: Managing Global Scripts & Styles + +Some modules may require additional styles or scripts that need to be referenced in **index.html** file. It's not easy to find and update these types of references in Blazor apps. ABP offers a simple, powerful, and modular way to manage global style and scripts in Blazor apps. + +To update script & style references without worrying about dependencies, ordering, etc in a project, you can use the [bundle command](../../CLI.md#bundle). + +You can also add custom styles and scripts and let ABP manage them for you. In your Blazor project, you can create a class implementing `IBundleContributor` interface. + +`IBundleContributor` interface contains two methods. + +* `AddScripts(...)` +* `AddStyles(...)` + +Both methods get `BundleContext` as a parameter. You can add scripts and styles to the `BundleContext` and run [bundle command](../../CLI.md#bundle). Bundle command detects custom styles and scripts with module dependencies and updates `index.html` file. + +## Example Usage +```csharp +namespace MyProject.Blazor +{ + public class MyProjectBundleContributor : IBundleContributor + { + public void AddScripts(BundleContext context) + { + context.Add("site.js"); + } + + public void AddStyles(BundleContext context) + { + context.Add("main.css"); + context.Add("custom-styles.css"); + } + } +} +``` + +> There is a BundleContributor class implementing `IBundleContributor` interface coming by default with the startup templates. So, most of the time, you don't need to add it manually. + +## Bundling And Minification +`abp bundle` command offers bundling and minification support for client-side resources(JavaScript and CSS files). `abp bundle` command reads the `appsettings.json` file inside the Blazor project and bundles the resources according to the configuration. You can find the bundle configurations inside `AbpCli.Bundle` element. + +Here are the options that you can control inside the `appsettings.json` file. + +`Mode`: Bundling and minification mode. Possible values are +* `BundleAndMinify`: Bundle all the files into a single file and minify the content. +* `Bundle`: Bundle all files into a single file, but not minify. +* `None`: Add files individually, do not bundle. + +`Name`: Bundle file name. Default value is `global`. + +`Parameters`: You can define additional key/value pair parameters inside this section. `abp bundle` command automatically sends these parameters to the bundle contributors, and you can check these parameters inside the bundle contributor, take some actions according to these values. + +Let's say that you want to exclude some resources from the bundle and control this action using the bundle parameters. You can add a parameter to the bundle section like below. + +```json +"AbpCli": { + "Bundle": { + "Mode": "BundleAndMinify", /* Options: None, Bundle, BundleAndMinify */ + "Name": "global", + "Parameters": { + "ExcludeThemeFromBundle":"true" + } + } + } +``` + +You can check this parameter and take action like below. + +```csharp +public class MyProjectNameBundleContributor : IBundleContributor +{ + public void AddScripts(BundleContext context) + { + } + + public void AddStyles(BundleContext context) + { + var excludeThemeFromBundle = bool.Parse(context.Parameters.GetValueOrDefault("ExcludeThemeFromBundle")); + context.Add("mytheme.css", excludeFromBundle: excludeThemeFromBundle); + context.Add("main.css"); + } +} +``` + diff --git a/docs/en/UI/Blazor/Localization.md b/docs/en/UI/Blazor/Localization.md index e6cf761ce3..d42249388e 100644 --- a/docs/en/UI/Blazor/Localization.md +++ b/docs/en/UI/Blazor/Localization.md @@ -1,3 +1,78 @@ # Blazor UI: Localization -Blazor applications can reuse the same `IStringLocalizer` service that is explained in the [localization document](../../Localization.md). All the localization resources and texts available in the server side are usable in the Blazor application. \ No newline at end of file +Blazor applications can reuse the same `IStringLocalizer` service that is explained in the [localization document](../../Localization.md). + +All the localization resources and texts available in the server side are usable in the Blazor application. + +## IStringLocalizer + +`IStringLocalizer` (`T` is the localization resource class) can be injected in any service or component to use the localization service. + +### Razor Components + +Use `@inject IStringLocalizer` to use the localization in a razor component. + +**Example: Localization in a Razor Component** + +````csharp +@page "/" +@using MyCompanyName.MyProjectName.Localization +@using Microsoft.Extensions.Localization +@inject IStringLocalizer L + +

+ @L["LongWelcomeMessage"] +

+```` + +> `L` is a name that we love and use as the name of a `IStringLocalizer` instance, while you can give any name. + +#### The AbpComponentBase + +`AbpComponentBase` is a useful base class that you can derive the components from. It has some useful properties/methods you typically need in a component. + +The `AbpComponentBase` already defines a base `L` property (of type `IStringLocalizer`). It only requires to set the resource type (in the constructor of the derived class). If you created your application from the ABP's application startup template, then you should have a *YourProjectComponentBase* class in the Blazor project. Inherit components from this class to have the localizer pre-injected. + +**Example: Derive from the base component class** + +````csharp +@page "/" +@inherits MyProjectNameComponentBase + +

+ @L["LongWelcomeMessage"] +

+```` + +### Other Services + +`IStringLocalizer` can be injected into any service. + +**Example** + +````csharp +public class MyService : ITransientDependency +{ + private readonly IStringLocalizer _localizer; + + public MyService(IStringLocalizer localizer) + { + _localizer = localizer; + } + + public void Foo() + { + var str = _localizer["HelloWorld"]; + } +} +```` + +### Format Arguments + +Format arguments can be passed after the localization key. If your message is `Hello {0}, welcome!`, then you can pass the `{0}` argument to the localizer like `_localizer["HelloMessage", "John"]`. + +> Refer to the [Microsoft's localization documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) for details about using the localization. + +## See Also + +* [Localization](../../Localization.md) \ No newline at end of file diff --git a/docs/en/UI/Blazor/Message.md b/docs/en/UI/Blazor/Message.md new file mode 100644 index 0000000000..a2faee2548 --- /dev/null +++ b/docs/en/UI/Blazor/Message.md @@ -0,0 +1,129 @@ +# Blazor UI: Message Service + +UI message service is used to show nice-looking messages to the user as a blocking dialog. + +## Quick Example + +Simply [inject](../../Dependency-Injection.md) `IUiMessageService` to your page or component and call the `Success` method to show a success message. + +```csharp +namespace MyProject.Blazor.Pages +{ + public partial class Index + { + private readonly IUiMessageService _uiMessageService; + + public Index(IUiMessageService uiMessageService) + { + _uiMessageService = uiMessageService; + } + + public async Task SaveAsync() + { + await _uiMessageService.Success( + "Your changes have been successfully saved!", + "Congratulations"); + } + } +} +``` + +It will show a dialog on the UI: + +![blazor-message-success](../../images/blazor-message-success.png) + +If you inherit your page or component from the `AbpComponentBase` class, you can use the `Message` property to access the `IUiMessageService` as a pre-injected property. + +```csharp +namespace MyProject.Blazor.Pages +{ + public partial class Index : AbpComponentBase + { + public async Task SaveAsync() + { + await Message.Success( + "Your changes have been successfully saved!", + "Congratulations"); + } + } +} +``` +> You typically use `@inherits AbpComponentBase` in the `.razor` file to inherit from the `AbpComponentBase`, instead of inheriting in the code behind file. + +## Informative Messages + +There are four types of informative message functions: + +* `Info(...)` +* `Success(...)` +* `Warn(...)` +* `Error(...)` + +All of these methods get three parameters: + +* `message`: The message (`string`) to be shown. +* `title`: An optional (`string`) title. +* `options`: An optional (`Action`) to configure UI message options. + +**Example: Show an error message** + +````csharp +_uiMessageService.Error('Your credit card number is not valid!'); +```` + +![blazor-message-success](../../images/blazor-message-error.png) + + +## Confirmation Message + +`IUiMessageService.Confirm(...)` method can be used to get a confirmation from the user. + +**Example** + +Use the following code to get a confirmation result from the user: + +```csharp +public async Task DeleteAsync() +{ + var confirmed = await _uiMessageService.Confirm("Are you sure to delete the 'admin' role?"); + if(confirmed) + { + //Delete the 'admin' role here. + } +} +``` + +The resulting UI will be like shown below: + +![blazor-message-confirm](../../images/blazor-message-confirm.png) + +If the user has clicked the `Yes` button, the `Confirm` method's return value will be `true`. + +## Configuration + +It is easy to change default message options if you like to it per message. Provide an `action` to the `options` parameter as shown below. + +```csharp +await _uiMessageService.Success( + "Your changes have been successfully saved!", + "Congratulations", + (options) => + { + options.MessageIcon = "msg-icon-new"; + options.CenterMessage = false; + }); +``` + +List of the options that you can change by providing the `action` parameter. + +* `CenterMessage` : (Default: true) If true, the message dialogue will be centered on the screen. +* `ShowMessageIcon` : (Default: true) If true, the message dialogue will show the large icon for the current message type. +* `MessageIcon` : Overrides the build-in message icon. +* `OkButtonText` : Custom text for the OK button. +* `OkButtonIcon` : Custom icon for the OK button. +* `ConfirmButtonText` : Custom text for the Confirmation button. +* `ConfirmButtonIcon` : Custom icon for the Confirmation button. +* `CancelButtonText` : Custom text for the Cancel button. +* `CancelButtonIcon` : Custom icon for the Cancel button. + +> "Confirm", "Cancel" and "Yes" texts are automatically localized based on the current language. \ No newline at end of file diff --git a/docs/en/UI/Blazor/Navigation-Menu.md b/docs/en/UI/Blazor/Navigation-Menu.md new file mode 100644 index 0000000000..7f09a65dbb --- /dev/null +++ b/docs/en/UI/Blazor/Navigation-Menu.md @@ -0,0 +1,207 @@ +# Blazor UI: Navigation / Menu + +Every application has a main menu to allow users to navigate to pages/screens of the application. Some applications may contain more than one menu in different sections of the UI. + +ABP Framework is a [modular](../../Module-Development-Basics.md) application development framework. **Every module may need to add items to the menu**. + +So, ABP Framework **provides a menu infrastructure** where; + +* The application or the modules can add items to a menu, without knowing how the menu is rendered. +* The [theme](Theming.md) properly renders the menu. + +## Adding Menu Items + +In order to add menu items (or manipulate the existing items) you need to create a class implementing the `IMenuContributor` interface. + +> The [application startup template](../../Startup-Templates/Application.md) already contains an implementation of the `IMenuContributor`. So, you can add items inside that class instead of creating a new one. + +**Example: Add a *CRM* menu item with *Customers* and *Orders* sub menu items** + +```csharp +using System.Threading.Tasks; +using MyProject.Localization; +using Volo.Abp.UI.Navigation; + +namespace MyProject.Web.Menus +{ + public class MyProjectMenuContributor : IMenuContributor + { + public async Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + await ConfigureMainMenuAsync(context); + } + } + + private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) + { + var l = context.GetLocalizer(); + + context.Menu.AddItem( + new ApplicationMenuItem("MyProject.Crm", l["Menu:CRM"]) + .AddItem(new ApplicationMenuItem( + name: "MyProject.Crm.Customers", + displayName: l["Menu:Customers"], + url: "/crm/customers") + ).AddItem(new ApplicationMenuItem( + name: "MyProject.Crm.Orders", + displayName: l["Menu:Orders"], + url: "/crm/orders") + ) + ); + } + } +} +``` + +* This example adds items only to the main menu (`StandardMenus.Main`: see the *Standard Menus* section below). +* It gets a `IStringLocalizer` from `context` to [localize](../../Localization.md) the display names of the menu items. +* Adds the Customers and Orders as children of the CRM menu. + +Once you create a menu contributor, you need to add it to the `AbpNavigationOptions` in the `ConfigureServices` method of your module: + +````csharp +Configure(options => +{ + options.MenuContributors.Add(new MyProjectMenuContributor()); +}); +```` + +This example uses some localization keys as display names those should be defined in the localization file: + +````json +"Menu:CRM": "CRM", +"Menu:Orders": "Orders", +"Menu:Customers": "Customers" +```` + +See the [localization document](../../Localization.md) to learn more about the localization. + +When you run the application, you will see the menu items added to the main menu: + +![nav-main-menu](../../images/nav-main-menu.png) + +> The menu is rendered by the current UI [theme](Theming.md). So, the look of the main menu can be completely different based on your theme. + +Here, a few notes on the menu contributors; + +* ABP Framework calls the `ConfigureMenuAsync` method **whenever need to render** the menu. +* Every menu item can have **children**. So, you can add menu items with **unlimited depth** (however, your UI theme may not support unlimited depth). +* Only leaf menu items have `url`s normally. When you click to a parent menu, its sub menu is opened or closed, you don't navigate the `url` of a parent menu item. +* If a menu item has no children and has no `url` defined, then it is not rendered on the UI. This simplifies to authorize the menu items: You only authorize the child items (see the next section). If none of the children are authorized, then the parent automatically disappears. + +### Menu Item Properties + +There are more options of a menu item (the constructor of the `ApplicationMenuItem` class). Here, the list of all available options; + +* `name` (`string`, required): The **unique name** of the menu item. +* `displayName` (`string`, required): Display name/text of the menu item. You can [localize](../../Localization.md) this as shown before. +* `url` (`string`): The URL of the menu item. +* `icon` (`string`): An icon name. Free [Font Awesome](https://fontawesome.com/) icon classes are supported out of the box. Example: `fa fa-book`. You can use any CSS font icon class as long as you include the necessary CSS files to your application. +* `order` (`int`): The order of the menu item. Default value is `1000`. Items are sorted by the adding order unless you specify an order value. +* `customData` (`object`): A custom object that you can associate to the menu item and use it while rendering the menu item. +* `target` (`string`): Target of the menu item. Can be `null` (default), "\_*blank*", "\_*self*", "\_*parent*", "\_*top*" or a frame name for web applications. +* `elementId` (`string`): Can be used to render the element with a specific HTML `id` attribute. +* `cssClass` (`string`): Additional string classes for the menu item. + +### Authorization + +As seen above, a menu contributor contributes to the menu dynamically. So, you can perform any custom logic or get menu items from any source. + +One use case is the [authorization](Authorization.md). You typically want to add menu items by checking a permission. + +**Example: Check if the current user has a permission** + +````csharp +if (await context.IsGrantedAsync("MyPermissionName")) +{ + //...add menu items +} +```` + +> You can use `context.AuthorizationService` to directly access to the `IAuthorizationService`. + +### Resolving Dependencies + +`context.ServiceProvider` can be used to resolve any service dependency. + +**Example: Get a service** + +````csharp +var myService = context.ServiceProvider.GetRequiredService(); +//...use the service +```` + +> You don't need to care about releasing/disposing services. ABP Framework handles it. + +### The Administration Menu + +There is a special menu item in the menu that is added by the ABP Framework: The *Administration* menu. It is typically used by the pre-built admin [application modules](../../Modules/Index.md): + +![nav-main-menu-administration](../../images/nav-main-menu-administration.png) + +If you want to add menu items under the *Administration* menu item, you can use the `context.Menu.GetAdministration()` extension method: + +````csharp +context.Menu.GetAdministration().AddItem(...) +```` + +### Manipulating the Existing Menu Items + +ABP Framework executes the menu contributors by the [module dependency order](../../Module-Development-Basics.md). So, you can manipulate the menu items that your application or module (directly or indirectly) depends on. + +**Example: Set an icon for the `Users` menu item added by the [Identity Module](../../Modules/Identity.md)** + +````csharp +var userMenu = context.Menu.FindMenuItem(IdentityMenuNames.Users); +userMenu.Icon = "fa fa-users"; +```` + +> `context.Menu` gives you ability to access to all the menu items those have been added by the previous menu contributors. + +## Standard Menus + +A menu is a **named** component. An application may contain more than one menus with different, unique names. There are two pre-defined standard menus: + +* `Main`: The main menu of the application. Contains links to the page of the application. Defined as a constant: `Volo.Abp.UI.Navigation.StandardMenus.Main`. +* `User`: User profile menu. Defined as a constant: `Volo.Abp.UI.Navigation.StandardMenus.User`. + +The `Main` menu already covered above. The `User` menu is available when a user has logged in: + +![user-menu](../../images/user-menu.png) + +You can add items to the `User` menu by checking the `context.Menu.Name` as shown below: + +```csharp +if (context.Menu.Name == StandardMenus.User) +{ + //...add items +} +``` + +## IMenuManager + +`IMenuManager` is generally used by the UI [theme](Theming.md) to render the menu items on the UI. So, **you generally don't need to directly use** the `IMenuManager`. + +**Example: Get the Main Menu to render in a razor component** + +```csharp +// Code behind file of a razor component +public partial class NavMenu +{ + private readonly IMenuManager _menuManager; + + public NavMenu(IMenuManager menuManager) + { + _menuManager = menuManager; + } + + protected override async Task OnInitializedAsync() + { + var menu = await _menuManager.GetAsync(StandardMenus.Main); + //... + } +} +``` + diff --git a/docs/en/UI/Blazor/Notification.md b/docs/en/UI/Blazor/Notification.md new file mode 100644 index 0000000000..24b589b75c --- /dev/null +++ b/docs/en/UI/Blazor/Notification.md @@ -0,0 +1,103 @@ +# Blazor UI: Notification Service + +`IUiNotificationService` is used to show toast style notifications on the user interface. + +## Quick Example + +Simply [inject](../../Dependency-Injection.md) `IUiNotificationService` to your page or component and call the `Success` method to show a success message. + +```csharp +namespace MyProject.Blazor.Pages +{ + public partial class Index + { + private readonly IUiNotificationService _uiNotificationService; + + public Index(IUiNotificationService uiNotificationService) + { + _uiNotificationService = uiNotificationService; + } + + public async Task DeleteAsync() + { + await _uiNotificationService.Success( + "The product 'Acme Atom Re-Arranger' has been successfully deleted." + ); + } + } +} +``` + +![blazor-notification-sucess](../../images/blazor-notification-success.png) + +If you inherit your page or component from the `AbpComponentBase` class, you can use the the `Notify` property to access the `IUiNotificationService` as a pre-injected property. + +```csharp +namespace MyProject.Blazor.Pages +{ + public partial class Index : AbpComponentBase + { + public async Task DeleteAsync() + { + await Notify.Success( + "The product 'Acme Atom Re-Arranger' has been successfully deleted." + ); + } + } +} +``` + +> You typically use `@inherits AbpComponentBase` in the `.razor` file to inherit from the `AbpComponentBase`, instead of inheriting in the code behind file. + +## Notification Types + +There are four types of pre-defined notifications; + +* `Info(...)` +* `Success(...)` +* `Warn(...)` +* `Error(...)` + +All of the methods above gets the following parameters; + +* `message`: The message (`string`) to be shown. +* `title`: An optional (`string`) title. +* `options`: An optional (`Action`) to configure notification options. + +## Configuration + +### Per Notification + +It is easy to change default notification options if you like to customize it per notification. Provide an action to the `options` parameter as shown below: + +```csharp +await UiNotificationService.Success( + "The product 'Acme Atom Re-Arranger' has been successfully deleted.", + options: (options) => + { + options.OkButtonText = + LocalizableString.Create("CustomOK"); + }); +``` + +### Available Options + +Here, the list of all available options; + +* `OkButtonText` : Custom text for the OK button. +* `OkButtonIcon` : Custom icon for the OK button + +### Global Configuration + +You can also configure global notification options to control the it in a single point. Configure the `UiNotificationOptions` [options class](../../Options.md) in the `ConfigureServices` of your [module](../../Module-Development-Basics.md): + +````csharp +Configure(options => +{ + options.OkButtonText = LocalizableString.Create("CustomOK"); +}); +```` + +The same options are available here. + +> *Per notification* configuration overrides the default values. \ No newline at end of file diff --git a/docs/en/UI/Blazor/Overall.md b/docs/en/UI/Blazor/Overall.md index 74c469e3c4..a249dd354b 100644 --- a/docs/en/UI/Blazor/Overall.md +++ b/docs/en/UI/Blazor/Overall.md @@ -1,6 +1,161 @@ -# Blazor UI for the ABP Framework +# Blazor UI: Overall -The detailed documentation for the Blazor UI is in progress. However, you can follow the documents below to start with the Blazor UI today. +## Introduction -* [Get started](https://docs.abp.io/en/abp/latest/Getting-Started?UI=Blazor) with the Blazor UI for the ABP Framework. -* [Web Application Development Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=Blazor) with the Blazor UI. \ No newline at end of file +[Blazor](https://docs.microsoft.com/en-us/aspnet/core/blazor/) is a framework for building interactive client-side web UI with .NET. It is promising for a .NET developer that you can create Single-Page Web Applications using C# and the Razor syntax. + +ABP provides infrastructure and integrations that make your Blazor development even easier, comfortable and enjoyable. + +This document provides an overview for the ABP Framework Blazor UI integration and highlights some major features. + +### Getting Started + +You can follow the documents below to start with the ABP Framework and the Blazor UI now: + +* [Get started](../../Getting-Started.md) with the Blazor UI for the ABP Framework. +* [Web Application Development Tutorial](../../Tutorials/Part-1.md) with the Blazor UI. + +## Modularity + +[Modularity](../../Module-Development-Basics.md) is one of the key goals of the ABP Framework. It is not different for the UI; It is possible to develop modular applications and reusable application modules with isolated and reusable UI pages and components. + +The [application startup template](../../Startup-Templates/Application.md) comes with some application modules pre-installed. These modules have their own UI pages embedded into their own NuGet packages. You don't see their code in your solution, but they work as expected on runtime. + +## Dynamic C# Client Proxies + +Dynamic C# Client Proxy system makes extremely easy to consume server side HTTP APIs from the UI. You just **inject** the [application service](../../Application-Services.md) **interface** and consume the remote APIs just like using local service method calls. + +**Example: Get list of books from server and list on the UI** + +````csharp +@page "/books" +@using Acme.BookStore.Books +@using Volo.Abp.Application.Dtos +@inject IBookAppService BookAppService + +
    + @foreach (var book in Books) + { +
  • + @book.Name (by @book.AuthorName) +
  • + } +
+ +@code { + private IReadOnlyList Books { get; set; } = new List(); + + protected override async Task OnInitializedAsync() + { + var result = await BookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + + Books = result.Items; + } +} +```` + +* This razor component (page) uses `@inject IBookAppService BookAppService` to get a reference to the service proxy. +* It uses `BookAppService.GetListAsync` in the `OnInitializedAsync` and gets the list of the books, just like a regular C# method call. +* Finally, the page renders the books in a list on the UI. + +ABP Framework handles all the low level details for you, including a proper HTTP call, JSON serialization, exception handling and authentication. + +See the [Dynamic C# Client Proxies](../../API/Dynamic-CSharp-API-Clients.md) document for more. + +## Theming System + +ABP Framework provides a complete [Theming](Theming.md) system with the following goals: + +* Reusable [application modules](../../Modules/Index.md) are developed **theme-independent**, so they can work with any UI theme. +* UI theme is **decided by the final application**. +* The theme is distributed as NuGet package, so it is **easily upgradable**. +* The final application can **customize** the selected theme. + +### Current Themes + +Currently, two themes are **officially provided**: + +* The [Basic Theme](Basic-Theme.md) is the minimalist theme with the plain Bootstrap style. It is **open source and free**. +* The [Lepton Theme](https://commercial.abp.io/themes) is a **commercial** theme developed by the core ABP team and is a part of the [ABP Commercial](https://commercial.abp.io/) license. + +### Base Libraries + +There are a set of standard libraries that comes pre-installed and supported by all the themes: + +* [Twitter Bootstrap](https://getbootstrap.com/) as the fundamental HTML/CSS framework. +* [Blazorise](https://github.com/stsrki/Blazorise) as a component library that supports the Bootstrap and adds extra components like Data Grid and Tree. +* [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. +* [Flag Icon](https://github.com/lipis/flag-icon-css) as a library to show flags of countries. + +These libraries are selected as the base libraries and available to the applications and modules. + +> Bootstrap's JavaScript part is not used since the Blazorise library already provides the necessary functionalities to the Bootstrap components in a native way. + +### The Layout + +The themes provide the layout. So, you have a responsive layout with the standard features already implemented. The screenshot below has taken from the layout of the [Basic Theme](Basic-Theme.md): + +![basic-theme-application-layout](../../images/basic-theme-application-layout.png) + +See the [Theming](Theming.md) document for more layout options and other details. + +### Layout Parts + +A typical layout consists of multiple parts. The [Theming](Theming.md) system provides [menus](Navigation-Menu.md), [toolbars](Toolbars.md), [page alerts](Page-Alerts.md) and more to dynamically control the layout by your application and the modules you are using. + +## Global Styles & Scripts / Bundling & Minification + +ABP provides a standard way to manage the global script and style dependencies of an application. This is an essential feature for modularity since some modules may have such dependencies and they can declare dependencies in that way. + +See the [Managing Global Scripts & Styles](Global-Scripts-Styles.md) document. + +## Services + +ABP provides useful services that you can consume in your applications. Some of them are; + +* [IUiMessageService](Message.md) is used to show modal messages to the user. +* [IUiNotificationService](Notification.md) is used to show toast-style notifications. +* [IAlertManager](Page-Alerts.md) is used to show in-page alerts. +* [ISettingProvider](Settings.md) is used to access to the current setting values. +* `ICurrentUser` and `ICurrentTenant` is used to get information about the current user and the tenant. + +## Dependency Injection + +Razor components doesn't support [constructor injection](../../Dependency-Injection.md) by default. ABP makes possible to inject dependencies into the constructor of the code-behind file of a component. + +**Example: Constructor-inject a service in the code-behind file of a component** + +````csharp +using Microsoft.AspNetCore.Components; + +namespace MyProject.Blazor.Pages +{ + public partial class Index + { + private readonly NavigationManager _navigationManager; + + public Index(NavigationManager navigationManager) + { + _navigationManager = navigationManager; + } + } +} +```` + +ABP makes this possible by auto registering components to and resolving the component from the [Dependency Injection](../../Dependency-Injection.md) system. + +> You can still continue to use property injection and the standard `[Inject]` approach if you prefer. + +Resolving a component from the Dependency Injection system makes it possible to easily replace components of a depended module. + +## Error Handling + +Blazor, by default, shows a yellow line at the bottom of the page if any unhandled exception occurs. However, this is not useful in a real application. + +ABP provides an [automatic error handling system](Error-Handling.md) for the Blazor UI. + +## Customization + +While the theme and some modules come as NuGet packages, you can still replace/override and customize them on need. See the [Customization / Overriding Components](Customization-Overriding-Components.md) document. \ No newline at end of file diff --git a/docs/en/UI/Blazor/Page-Alerts.md b/docs/en/UI/Blazor/Page-Alerts.md new file mode 100644 index 0000000000..cfac721572 --- /dev/null +++ b/docs/en/UI/Blazor/Page-Alerts.md @@ -0,0 +1,62 @@ +# Blazor UI: Page Alerts + +It is common to show error, warning or information alerts to inform the user. An example *Service Interruption* alert is shown below: + +![blazor-page-alert-example](../../images/blazor-page-alert-example.png) + +## Quick Example + +Simply [inject](../../Dependency-Injection.md) `IAlertManager` to your page or component and call the `Alerts.Warning` method to show a success message. + +```csharp +namespace MyProject.Blazor.Pages +{ + public partial class Index + { + private readonly IAlertManager _alertManager; + + public Index(IAlertManager alertManager) + { + this._alertManager = alertManager; + } + + protected override void OnInitialized() + { + _alertManager.Alerts.Warning( + "We will have a service interruption between 02:00 AM and 04:00 AM at October 23, 2023!", + "Service Interruption"); + base.OnInitialized(); + } + } +} +``` + +If you inherit your page or component from the `AbpComponentBase` class, you can use the `Alerts` property to add alerts. + +```csharp +namespace MyProject.Blazor.Pages +{ + public partial class Index : AbpComponentBase + { + protected override void OnInitialized() + { + Alerts.Warning( + "We will have a service interruption between 02:00 AM and 04:00 AM at October 23, 2023!", + "Service Interruption"); + base.OnInitialized(); + } + } +} +``` + +> You typically use `@inherits AbpComponentBase` in the `.razor` file to inherit from the `AbpComponentBase`, instead of inheriting in the code behind file. + +### Alert Types + +`Warning` is used to show a warning alert. Other common methods are `Info`, `Danger` and `Success`. + +Beside the standard methods, you can use the `Alerts.Add` method by passing an `AlertType` `enum` with one of these values: `Default`, `Primary`, `Secondary`, `Success`, `Danger`, `Warning`, `Info`, `Light`, `Dark`. + +### Dismissible + +All alert methods gets an optional `dismissible` parameter. Default value is `true` which makes the alert box dismissible. Set it to `false` to create a sticky alert box. diff --git a/docs/en/UI/Blazor/Page-Header.md b/docs/en/UI/Blazor/Page-Header.md new file mode 100644 index 0000000000..2c7921bbd2 --- /dev/null +++ b/docs/en/UI/Blazor/Page-Header.md @@ -0,0 +1,3 @@ +# Blazor UI: Page Header + +TODO \ No newline at end of file diff --git a/docs/en/UI/Blazor/Page-Progress.md b/docs/en/UI/Blazor/Page-Progress.md new file mode 100644 index 0000000000..b1d5b0df12 --- /dev/null +++ b/docs/en/UI/Blazor/Page-Progress.md @@ -0,0 +1,57 @@ +# Blazor UI: Page Progress + +Page Progress is used to show a progress bar indicator on top of the page and to show to the user that currently a long running process is in the work. + +By default you don't need to do anything to show the progress indicator, as all the work is done automatically by the ABP Framework internals. This means that all calls to the ABP backend (through your HTTP API) will activate page progress and show the loading indicator. + +This doesn't mean that you don't have the control over it. On the contrary, if you want to show progress for your own processes, it is really easy to do. All you have to do is to use inject and use the `IUiPageProgressService`. + +## Example + +First, inject the `IUiPageProgressService` into your page/component. + +```cs +@inject IUiPageProgressService pageProgressService +``` + +Next, invoke the `Go` method in `IUiPageProgressService`. It's that simple: + +```cs +Task OnClick() +{ + return pageProgressService.Go(null); +} +``` + +The previous example will show the progress with a default settings. If, for example you want to change the progress color you can override it by setting the options through the `Go` method. + +```cs +Task OnClick() +{ + return pageProgressService.Go(null, options => + { + options.Type = UiPageProgressType.Warning; + }); +} +``` + +## Breakdown + +The first parameter of the `Go` needs a little explanation. In the previous example we have set it to `null` which means, once called it will show an _indeterminate_ indicator and will cycle the loading animation indefinitely, until we hide the progress. You also have the option of defining the actual percentage of the progress and the code is the same, just instead of sending it the `null` you will send it a number between `0` and `100`. + +```cs +pageProgressService.Go(25) +``` + +### Valid values + +1. `null` - show _indeterminate_ indicator +2. `>= 0` and `<= 100` - show the regular _percentage_ progress + +### Hiding progress + +To hide the progress just set the actual values to something other then the _Valid value_. + +```cs +pageProgressService.Go(-1) +``` \ No newline at end of file diff --git a/docs/en/UI/Blazor/Routing.md b/docs/en/UI/Blazor/Routing.md new file mode 100644 index 0000000000..31a5637b51 --- /dev/null +++ b/docs/en/UI/Blazor/Routing.md @@ -0,0 +1,24 @@ +# Blazor UI: Routing + +Blazor has its own [routing system](https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing) and you can use it in your applications. ABP doesn't add any new feature to it, except one small improvement for the [modular development](../../Module-Development-Basics.md). + +## AbpRouterOptions + +Blazor `Router` component requires to define `AdditionalAssemblies` when you have components in assemblies/projects other than the main application's entrance assembly. So, if you want to create razor class libraries as ABP modules, you typically want to add the module's assembly to the `AdditionalAssemblies`. In this case, you need to add your module's assembly to the `AbpRouterOptions`. + +**Example** + +````csharp +Configure(options => +{ + options.AdditionalAssemblies.Add(typeof(MyBlazorModule).Assembly); +}); +```` + +Write this code in the `ConfigureServices` method of your [module](../../Module-Development-Basics.md). + +`AbpRouterOptions` has another property, `AppAssembly`, which should be the entrance assembly of the application and typically set in the final application's module. If you've created your solution with the [application startup template](../../Startup-Templates/Application.md), it is already configured for you. + +## See Also + +* [Blazor Routing](https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing) (Microsoft Documentation) \ No newline at end of file diff --git a/docs/en/UI/Blazor/Services/Notification.md b/docs/en/UI/Blazor/Services/Notification.md deleted file mode 100644 index 0e5ababa0e..0000000000 --- a/docs/en/UI/Blazor/Services/Notification.md +++ /dev/null @@ -1,3 +0,0 @@ -# Blazor UI Notification - -`UiNotificationService` is used to show toastr style notifications on the user interface. The documentation is in progress... \ No newline at end of file diff --git a/docs/en/UI/Blazor/Settings.md b/docs/en/UI/Blazor/Settings.md index 276462c411..e146b7ea69 100644 --- a/docs/en/UI/Blazor/Settings.md +++ b/docs/en/UI/Blazor/Settings.md @@ -1,3 +1,63 @@ # Blazor UI: Settings -Blazor applications can reuse the same `ISettingProvider` service that is explained in the [settings document](../../Settings.md). \ No newline at end of file +Blazor applications can reuse the same `ISettingProvider` service that is explained in the [settings document](../../Settings.md). + +## ISettingProvider + +`ISettingProvider` is used to get the value of a setting or get the values of all the settings. + +**Example usages in a simple service** + +````csharp +public class MyService : ITransientDependency +{ + private readonly ISettingProvider _settingProvider; + + //Inject ISettingProvider in the constructor + public MyService(ISettingProvider settingProvider) + { + _settingProvider = settingProvider; + } + + public async Task FooAsync() + { + //Get a value as string. + string setting1 = await _settingProvider.GetOrNullAsync("MySettingName"); + + //Get a bool value and fallback to the default value (false) if not set. + bool setting2 = await _settingProvider.GetAsync("MyBoolSettingName"); + + //Get a bool value and fallback to the provided default value (true) if not set. + bool setting3 = await _settingProvider.GetAsync( + "MyBoolSettingName", defaultValue: true); + + //Get a bool value with the IsTrueAsync shortcut extension method + bool setting4 = await _settingProvider.IsTrueAsync("MyBoolSettingName"); + + //Get an int value or the default value (0) if not set + int setting5 = (await _settingProvider.GetAsync("MyIntegerSettingName")); + + //Get an int value or null if not provided + int? setting6 = (await _settingProvider + .GetOrNullAsync("MyIntegerSettingName"))?.To(); + } +} +```` + +**Example usage in a Razor Component** + +````csharp +@page "/" +@using Volo.Abp.Settings +@inject ISettingProvider SettingProvider +@code { + protected override async Task OnInitializedAsync() + { + bool settingValue = await SettingProvider.GetAsync("MyBoolSettingName"); + } +} +```` + +## See Also + +* [Settings](../../Settings.md) \ No newline at end of file diff --git a/docs/en/UI/Blazor/Testing.md b/docs/en/UI/Blazor/Testing.md new file mode 100644 index 0000000000..08562c5861 --- /dev/null +++ b/docs/en/UI/Blazor/Testing.md @@ -0,0 +1,3 @@ +# Blazor: Testing + +Coming soon. \ No newline at end of file diff --git a/docs/en/UI/Blazor/Theming.md b/docs/en/UI/Blazor/Theming.md new file mode 100644 index 0000000000..00bf911ff9 --- /dev/null +++ b/docs/en/UI/Blazor/Theming.md @@ -0,0 +1,207 @@ +# Blazor UI: Theming + +## Introduction + +ABP Framework provides a complete **UI Theming** system with the following goals: + +* Reusable [application modules](../../Modules/Index.md) are developed **theme-independent**, so they can work with any UI theme. +* UI theme is **decided by the final application**. +* The theme is distributed via a NuGet package, so it is **easily upgradable**. +* The final application can **customize** the selected theme. + +In order to accomplish these goals, ABP Framework; + +* Determines a set of **base libraries** used and adapted by all the themes. So, module and application developers can depend on and use these libraries without depending on a particular theme. +* Provides a system that consists of layout parts (like [navigation menus](Navigation-Menu.md) and [toolbars](Toolbars.md)) that is implemented by all the themes. So, the modules and the application to contribute to the layout to compose a consistent application UI. + +### Current Themes + +Currently, two themes are **officially provided**: + +* The [Basic Theme](Basic-Theme.md) is the minimalist theme with the plain Bootstrap style. It is **open source and free**. +* The [Lepton Theme](https://commercial.abp.io/themes) is a **commercial** theme developed by the core ABP team and is a part of the [ABP Commercial](https://commercial.abp.io/) license. + +## Overall + +### The Base Libraries + +All the themes must depend on the [Volo.Abp.AspNetCore.Components.WebAssembly.Theming](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Components.WebAssembly.Theming) NuGet package, so they are indirectly depending on the following libraries: + +* [Twitter Bootstrap](https://getbootstrap.com/) as the fundamental HTML/CSS framework. +* [Blazorise](https://github.com/stsrki/Blazorise) as a component library that supports the Bootstrap and adds extra components like Data Grid and Tree. +* [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library. +* [Flag Icon](https://github.com/lipis/flag-icon-css) as a library to show flags of countries. + +These libraries are selected as the base libraries and available to the applications and modules. + +> Bootstrap's JavaScript part is not used since the Blazorise library already provides the necessary functionalities to the Bootstrap components in a native way. + +### The Layout + +All themes must define a layout for the application. The following image shows the user management page in the [Basic Theme](Basic-Theme.md) application layout: + +![basic-theme-application-layout-blazor](../../images/basic-theme-application-layout-blazor.png) + +And the same page is shown below with the [Lepton Theme](https://commercial.abp.io/themes) application layout: + +![lepton-theme-application-layout](../../images/lepton-theme-blazor-layout.png) + +As you can see, the page is the same, but the look is completely different in the themes above. + +The application layout typically includes the following parts; + +* A [main menu](Navigation-Menu.md) +* Main [Toolbar](Toolbars.md) with the following components; + * User menu + * Language switch dropdown +* [Page alerts](Page-Alerts.md) +* The page content (aka `@Body`) + +## Implementing a Theme + +A theme is simply a Razor Class Library. + +### The Easy Way + +The easiest way to create a new theme is to copy the [Basic Theme Source Code](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme) and customize it. Once you get a copy of the theme in your solution, remove the `Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme` NuGet package and reference to the local project. + +### Global Styles / Scripts + +A theme generally needs to add a global style to the page. ABP provides a system to manage the [Global Styles and Scripts](Global-Scripts-Styles.md). A theme can implement the `IBundleContributor` to add global style or script files to the page. + +**Example: Adding a style to the page** + +````csharp +using Volo.Abp.Bundling; + +namespace MyTheme +{ + public class MyThemeBundleContributor : IBundleContributor + { + public void AddScripts(BundleContext context) + { + + } + + public void AddStyles(BundleContext context) + { + context.Add("_content/MyTheme/styles.css"); + } + } +} +```` + +`styles.css` file should be added into the `wwwroot` folder of the theme project for this example. When you use the `abp bundle` command, this class is automatically discovered and executed to add the style to the page. + +See the [Global Styles and Scripts](Global-Scripts-Styles.md) document for more. + +### Layout Parts + +A typical Layout consists of several parts. The theme should include the necessary parts in each layout. + +**Example: The Basic Theme has the following parts for the Application Layout** + +![basic-theme-application-layout-parts](../../images/basic-theme-application-layout-parts.png) + +The application code and the modules can only show contents in the Page Content part. If they need to change the other parts (to add a menu item, to add a toolbar item, to change the application name in the branding area...) they should use the ABP Framework APIs. + +The following sections explain the fundamental parts pre-defined by the ABP Framework and can be implemented by the themes. + +> It is a good practice to split the layout into components/partials, so the final application can override them partially for customization purpose. + +#### Branding + +`IBrandingProvider` service should be used to get the name and the logo URL of the application to render in the Branding part. + +The [Application Startup Template](../../Startup-Templates/Application.md) has an implementation of this interface to set the values by the application developer. + +#### Main Menu + +`IMenuManager` service is used to get the main menu items and render on the layout. + +**Example: Get the Main Menu to render in a razor component** + +```csharp +// Code behind file of a razor component +public partial class NavMenu +{ + private readonly IMenuManager _menuManager; + + public NavMenu(IMenuManager menuManager) + { + _menuManager = menuManager; + } + + protected override async Task OnInitializedAsync() + { + var menu = await _menuManager.GetAsync(StandardMenus.Main); + //... + } +} +``` + +See the [Navigation / Menus](Navigation-Menu.md) document to learn more about the navigation system. + +#### Main Toolbar + +`IToolbarManager` service is used to get the Main Toolbar items and render on the layout. Each item of this toolbar is a Razor Component, so it may include any type of UI elements. Inject the `IToolbarManager` and use the `GetAsync` to get the toolbar items: + +````csharp +var toolbar = await _toolbarManager.GetAsync(StandardToolbars.Main); +```` + +> See the [Toolbars](Toolbars.md) document to learn more on the toolbar system. + +The theme has a responsibility to add two pre-defined items to the main toolbar: Language Selection and User Menu. To do that, create a class implementing the `IToolbarContributor` interface and add it to the `AbpToolbarOptions` as shown below: + +```csharp +Configure(options => +{ + options.Contributors.Add(new BasicThemeMainTopToolbarContributor()); +}); +``` + +##### Language Selection + +Language Selection toolbar item is generally a dropdown that is used to switch between languages. `ILanguageProvider` is used to get the list of available languages and `CultureInfo.CurrentUICulture` is used to learn the current language. + +Local Storage is used to get and set the current language with the `Abp.SelectedLanguage` key. + +**Example: Get the currently selected language** + +````csharp +var selectedLanguageName = await JsRuntime.InvokeAsync( + "localStorage.getItem", + "Abp.SelectedLanguage" + ); +```` + +**Example: Set the selected language** + +````csharp +await JsRuntime.InvokeVoidAsync( + "localStorage.setItem", + "Abp.SelectedLanguage", + "en-US" + ); +```` + +The theme should reload the page after changing the language: + +````csharp +await JsRuntime.InvokeVoidAsync("location.reload"); +```` + +##### User Menu + +User menu includes links related to the user account. `IMenuManager` is used just like the Main Menu, but this time with `StandardMenus.User` parameter like shown below: + +````csharp +var menu = await _menuManager.GetAsync(StandardMenus.User); +```` + +[ICurrentUser](../../CurrentUser.md) and [ICurrentTenant](../../Multi-Tenancy.md) services can be used to obtain the current user and tenant names. + +#### Page Alerts + +`IAlertManager` service is used to get the current page alerts to render on the layout. See the [Page Alerts](Page-Alerts.md) document to learn more. \ No newline at end of file diff --git a/docs/en/UI/Blazor/Toolbars.md b/docs/en/UI/Blazor/Toolbars.md new file mode 100644 index 0000000000..b8cca6077a --- /dev/null +++ b/docs/en/UI/Blazor/Toolbars.md @@ -0,0 +1,75 @@ +# Blazor UI: Toolbars + +The Toolbar system is used to define **toolbars** on the user interface. Modules (or your application) can add **items** to a toolbar, then the [theme](Theming.md) renders the toolbar on the **layout**. + +There is only one **standard toolbar** named "Main" (defined as a constant: `StandardToolbars.Main`). The [Basic Theme](Basic-Theme) renders the main toolbar as shown below: + +![bookstore-toolbar-highlighted](../../images/bookstore-toolbar-highlighted.png) + +In the screenshot above, there are two items added to the main toolbar: Language switch component & user menu. You can add your own items here. + +## Example: Add a Notification Icon + +In this example, we will add a **notification (bell) icon** to the left of the language switch item. A item in the toolbar should be a **Razor Component**. So, first, create a new razor component in your project (the location of the component doesn't matter): + +![bookstore-notification-view-component](../../images/blazor-notification-bell-component.png) + +The content of the `Notification.razor` is shown below: + +````html +@inherits Volo.Abp.AspNetCore.Components.AbpComponentBase +
+ +
+@code { + private async Task ShowNotifications() + { + await Message.Info("TODO: Show notifications"); + } +} +```` + +This sample simply shows a message. In real life, you probably want to call an HTTP API to get notifications and show on the UI. + +Now, we can create a class implementing the `IToolbarContributor` interface: + +````csharp +using System.Threading.Tasks; +using MyCompanyName.MyProjectName.Blazor.Components; +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars; + +namespace MyCompanyName.MyProjectName.Blazor +{ + public class MyToolbarContributor : IToolbarContributor + { + public Task ConfigureToolbarAsync(IToolbarConfigurationContext context) + { + if (context.Toolbar.Name == StandardToolbars.Main) + { + context.Toolbar.Items.Insert(0, new ToolbarItem(typeof(Notification))); + } + + return Task.CompletedTask; + } + } +} +```` + +This class adds the `NotificationViewComponent` as the first item in the `Main` toolbar. + +Finally, you need to add this contributor to the `AbpToolbarOptions`, in the `ConfigureServices` of your [module](../../Module-Development-Basics.md): + +````csharp +Configure(options => +{ + options.Contributors.Add(new MyToolbarContributor()); +}); +```` + +That's all, you will see the notification icon on the toolbar when you run the application: + +![bookstore-notification-icon-on-toolbar](../../images/bookstore-notification-icon-on-toolbar.png) + +## IToolbarManager + +`IToolbarManager` is used to render the toolbar. It returns the toolbar items by a toolbar name. This is generally used by the [themes](Theming.md) to render the toolbar on the layout. \ No newline at end of file diff --git a/docs/en/Upgrading.md b/docs/en/Upgrading.md index 1140c5d573..fcda081188 100644 --- a/docs/en/Upgrading.md +++ b/docs/en/Upgrading.md @@ -16,11 +16,26 @@ Run this command in the terminal while you are in the root folder of your soluti > If your solution has the Angular UI, you probably have `aspnet-core` and `angular` folders in the solution. Run this command in the parent folder of these two folders. -## The Blog Posts +### Database Migrations + +> Warning: Be careful if you are migrating your database since you may have data loss in some cases. Carefully check the generated migration code before executing it. It is suggested to take a backup of your current database. + +When you upgrade to a new version, it is good to check if there is a database schema change and upgrade your database schema if your database provider is **Entity Framework Core**; + +* Use `Add-Migration "Upgraded_To_Abp_4_1"` or a similar command in the Package Manager Console (PMC) to create a new migration (Set the `EntityFrameworkCore.DbMigrations` as the Default project in the PMC and `.DbMigrator` as the Startup Project in the Solution Explorer, in the Visual Studio). +* Run the `.DbMigrator` application to upgrade the database and seed the initial data. + +If `Add-Migration` generates an empty migration, you can use `Remove-Migration` to delete it before executing the `.DbMigrator`. + +## The Blog Posts & Guides Sometimes we introduce new features/changes that requires to make changes in the startup template. We already implement the changes in the startup template for new applications. However, in some cases you need to manually make some minor changes in your solution. -Whenever you upgrade your solution, it is strongly suggested to check the [ABP BLOG](https://blog.abp.io/?_ga=2.177248992.411298747.1597771169-1910388957.1594128976) to learn the new features and changes coming with the new version. We regularly publish posts and write these kind of changes. If the changes are not trivial, we also provide migration guides. +Whenever you upgrade your solution, it is strongly suggested to check the [ABP BLOG](https://blog.abp.io/) to learn the new features and changes coming with the new version. We regularly publish posts and write these kind of changes. + +### Migration Guides + +We prepare migration guides if the new version brings breaking changes for existing applications. See the [Migration Guides](Migration-Guides/Index.md) page for all the guides. ## Semantic Versioning & Breaking Changes diff --git a/docs/en/Value-Objects.md b/docs/en/Value-Objects.md index 0c6c8d4424..f61e32245b 100644 --- a/docs/en/Value-Objects.md +++ b/docs/en/Value-Objects.md @@ -1,3 +1,75 @@ -## Value Objects +# Value Objects -TODO \ No newline at end of file +> An object that represents a descriptive aspect of the domain with no conceptual identity is called a VALUE OBJECT. +> +> (Eric Evans) + +Two [Entities](Entities.md) with the same properties but with different `Id`s are considered as different entities. However, Value Objects have no `Id`s and they are considered as equals if they have the same property values. + +## The ValueObject Class + +`ValueObject` is an abstract class that can be inherited to create a Value Object class. + +**Example: An Address class** + +````csharp +public class Address : ValueObject +{ + public Guid CityId { get; private set; } + + public string Street { get; private set; } + + public int Number { get; private set; } + + private Address() + { + + } + + public Address( + Guid cityId, + string street, + int number) + { + CityId = cityId; + Street = street; + Number = number; + } + + protected override IEnumerable GetAtomicValues() + { + yield return Street; + yield return CityId; + yield return Number; + } +} +```` + +* A Value Object class must implement the `GetAtomicValues()` method to return the primitive values. + +### ValueEquals + +`ValueObject.ValueEquals(...)` method is used to check if two Value Objects are equals. + +**Example: Check if two addresses are equals** + +````csharp +Address address1 = ... +Address address2 = ... + +if (address1.ValueEquals(address2)) //Check equality +{ + ... +} +```` + +## Best Practices + +Here are some best practices when using Value Objects: + +- Design a value object as **immutable** (like the Address above) if there is not a good reason for designing it as mutable. +- The properties that make up a Value Object should form a conceptual whole. For example, CityId, Street and Number shouldn't be separate properties of a Person entity. This also makes the Person entity simpler. + +## See Also + +* [Entities](Entities.md) \ No newline at end of file diff --git a/docs/en/_resources/Diagrams.docx b/docs/en/_resources/Diagrams.docx index 424e281470..15cb38bab2 100644 Binary files a/docs/en/_resources/Diagrams.docx and b/docs/en/_resources/Diagrams.docx differ diff --git a/docs/en/_resources/ddd-microservice-simple.psd b/docs/en/_resources/ddd-microservice-simple.psd new file mode 100644 index 0000000000..ad0946f132 Binary files /dev/null and b/docs/en/_resources/ddd-microservice-simple.psd differ diff --git a/docs/en/_resources/ui-db-options.psd b/docs/en/_resources/ui-db-options.psd new file mode 100644 index 0000000000..2c98b76a07 Binary files /dev/null and b/docs/en/_resources/ui-db-options.psd differ diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 1f4e77541f..3fff6d67c1 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -21,6 +21,31 @@ } ] }, + { + "text": "Startup Templates", + "items": [ + { + "text": "Overall", + "path": "Startup-Templates/Index.md" + }, + { + "text": "Application", + "path": "Startup-Templates/Application.md" + }, + { + "text": "Module", + "path": "Startup-Templates/Module.md" + }, + { + "text": "Console", + "path": "Startup-Templates/Console.md" + }, + { + "text": "WPF", + "path": "Startup-Templates/WPF.md" + } + ] + }, { "text": "Tutorials", "items": [ @@ -68,29 +93,10 @@ "path": "Tutorials/Part-10.md" } ] - } - ] - }, - { - "text": "Guides", - "items": [ + }, { - "text": "Customizing the Application Modules", - "path": "Customizing-Application-Modules-Guide.md", - "items": [ - { - "text": "Extending Entities", - "path": "Customizing-Application-Modules-Extending-Entities.md" - }, - { - "text": "Overriding Services", - "path": "Customizing-Application-Modules-Overriding-Services.md" - }, - { - "text": "Overriding the User Interface", - "path": "Customizing-Application-Modules-Overriding-User-Interface.md" - } - ] + "text": "Community Articles", + "path": "https://community.abp.io/articles" }, { "text": "Migrating from the ASP.NET Boilerplate", @@ -98,34 +104,9 @@ } ] }, - { - "text": "CLI", - "path": "CLI.md" - }, - { - "text": "Authentication & Security", - "items": [ - { - "text": "Social/External Logins", - "path": "Authentication/Social-External-Logins.md" - }, - { - "text": "CSRF/XSRF & Anti Forgery", - "path": "CSRF-Anti-Forgery.md" - } - ] - }, { "text": "Fundamentals", "items": [ - { - "text": "Configuration", - "path": "Configuration.md" - }, - { - "text": "Options", - "path": "Options.md" - }, { "text": "Dependency Injection", "path": "Dependency-Injection.md", @@ -137,16 +118,16 @@ ] }, { - "text": "Virtual File System", - "path": "Virtual-File-System.md" + "text": "Configuration", + "path": "Configuration.md" }, { - "text": "Localization", - "path": "Localization.md" + "text": "Options", + "path": "Options.md" }, { - "text": "Exception Handling", - "path": "Exception-Handling.md" + "text": "Authorization", + "path": "Authorization.md" }, { "text": "Validation", @@ -159,8 +140,8 @@ ] }, { - "text": "Authorization", - "path": "Authorization.md" + "text": "Localization", + "path": "Localization.md" }, { "text": "Caching", @@ -173,83 +154,59 @@ ] }, { - "text": "Logging", - "path": "Logging.md" - }, - { - "text": "Audit Logging", - "path": "Audit-Logging.md" + "text": "Exception Handling", + "path": "Exception-Handling.md" }, { "text": "Settings", "path": "Settings.md" }, { - "text": "Features", - "path": "Features.md" - }, - { - "text": "Data Filtering", - "path": "Data-Filtering.md" + "text": "Connection Strings", + "path": "Connection-Strings.md" }, { "text": "Object Extensions", "path": "Object-Extensions.md" + }, + { + "text": "Logging", + "path": "Logging.md" } ] }, { - "text": "Event Bus", + "text": "Infrastructure", "items": [ { - "text": "Overall", - "path": "Event-Bus.md" - }, - { - "text": "Local Event Bus", - "path": "Local-Event-Bus.md" + "text": "Audit Logging", + "path": "Audit-Logging.md" }, { - "text": "Distributed Event Bus", - "path": "Distributed-Event-Bus.md", + "text": "Background Jobs", + "path": "Background-Jobs.md", "items": [ { - "text": "RabbitMQ Integration", - "path": "Distributed-Event-Bus-RabbitMQ-Integration.md" + "text": "Hangfire Integration", + "path": "Background-Jobs-Hangfire.md" }, { - "text": "Kafka Integration", - "path": "Distributed-Event-Bus-Kafka-Integration.md" + "text": "RabbitMQ Integration", + "path": "Background-Jobs-RabbitMq.md" }, { - "text": "Rebus Integration", - "path": "Distributed-Event-Bus-Rebus-Integration.md" + "text": "Quartz Integration", + "path": "Background-Jobs-Quartz.md" } ] - } - ] - }, - { - "text": "Services", - "items": [ - { - "text": "Current User", - "path": "CurrentUser.md" }, { - "text": "Object to object mapping", - "path": "Object-To-Object-Mapping.md" - }, - { - "text": "Email Sending", + "text": "Background Workers", + "path": "Background-Workers.md", "items": [ { - "text": "Email Sending System", - "path": "Emailing.md" - }, - { - "text": "MailKit Integration", - "path": "MailKit.md" + "text": "Quartz Integration", + "path": "Background-Workers-Quartz.md" } ] }, @@ -296,85 +253,177 @@ ] }, { - "text": "Text Templating", - "path": "Text-Templating.md" + "text": "CSRF/XSRF & Anti Forgery", + "path": "CSRF-Anti-Forgery.md" + }, + { + "text": "Current User", + "path": "CurrentUser.md" + }, + { + "text": "Data Filtering", + "path": "Data-Filtering.md" + }, + { + "text": "Data Seeding", + "path": "Data-Seeding.md" + }, + { + "text": "Email Sending", + "items": [ + { + "text": "Email Sending System", + "path": "Emailing.md" + }, + { + "text": "MailKit Integration", + "path": "MailKit.md" + } + ] + }, + { + "text": "Event Bus", + "items": [ + { + "text": "Overall", + "path": "Event-Bus.md" + }, + { + "text": "Local Event Bus", + "path": "Local-Event-Bus.md" + }, + { + "text": "Distributed Event Bus", + "path": "Distributed-Event-Bus.md", + "items": [ + { + "text": "RabbitMQ Integration", + "path": "Distributed-Event-Bus-RabbitMQ-Integration.md" + }, + { + "text": "Kafka Integration", + "path": "Distributed-Event-Bus-Kafka-Integration.md" + }, + { + "text": "Rebus Integration", + "path": "Distributed-Event-Bus-Rebus-Integration.md" + } + ] + } + ] + }, + { + "text": "Features", + "path": "Features.md" }, { "text": "GUID Generation", "path": "Guid-Generation.md" }, { - "text": "Timing", - "path": "Timing.md" - } - ] - }, - { - "text": "Multi Tenancy", - "path": "Multi-Tenancy.md" - }, - { - "text": "Module Development", - "items": [ + "text": "Object to Object Mapping", + "path": "Object-To-Object-Mapping.md" + }, { - "text": "Basics", - "path": "Module-Development-Basics.md" + "text": "Text Templating", + "path": "Text-Templating.md" }, { - "text": "Plug-In Modules" + "text": "Timing", + "path": "Timing.md" }, { - "text": "Best Practices", - "path": "Best-Practices/Index.md" + "text": "Virtual File System", + "path": "Virtual-File-System.md" } ] }, { - "text": "Domain Driven Design", + "text": "Architecture", "items": [ { - "text": "Overall", - "path": "Domain-Driven-Design.md" - }, - { - "text": "Domain Layer", + "text": "Modularity", "items": [ { - "text": "Entities & Aggregate Roots", - "path": "Entities.md" + "text": "Basics", + "path": "Module-Development-Basics.md" }, { - "text": "Value Objects" + "text": "Plug-In Modules", + "path": "PlugIn-Modules.md" }, { - "text": "Repositories", - "path": "Repositories.md" + "text": "Customizing/Extending Modules", + "path": "Customizing-Application-Modules-Guide.md" }, { - "text": "Domain Services" - }, - { - "text": "Specifications", - "path": "Specifications.md" + "text": "Best Practices", + "path": "Best-Practices/Index.md" } ] }, { - "text": "Application Layer", + "text": "Domain Driven Design", "items": [ { - "text": "Application Services", - "path": "Application-Services.md" + "text": "Overall", + "path": "Domain-Driven-Design.md" + }, + { + "text": "Domain Layer", + "items": [ + { + "text": "Entities & Aggregate Roots", + "path": "Entities.md" + }, + { + "text": "Value Objects", + "path": "Value-Objects.md" + }, + { + "text": "Repositories", + "path": "Repositories.md" + }, + { + "text": "Domain Services", + "path": "Domain-Services.md" + }, + { + "text": "Specifications", + "path": "Specifications.md" + } + ] }, { - "text": "Data Transfer Objects", - "path": "Data-Transfer-Objects.md" + "text": "Application Layer", + "items": [ + { + "text": "Application Services", + "path": "Application-Services.md" + }, + { + "text": "Data Transfer Objects", + "path": "Data-Transfer-Objects.md" + }, + { + "text": "Unit Of Work", + "path": "Unit-Of-Work.md" + } + ] }, { - "text": "Unit Of Work", - "path": "Unit-Of-Work.md" + "text": "Guide: Implementing DDD", + "path": "Domain-Driven-Design-Implementation-Guide.md" } ] + }, + { + "text": "Multi Tenancy", + "path": "Multi-Tenancy.md" + }, + { + "text": "Microservices", + "path": "Microservice-Architecture.md" } ] }, @@ -404,8 +453,12 @@ "text": "User Interface", "items": [ { - "text": "ASP.NET Core MVC / Razor Pages", + "text": "MVC / Razor Pages", "items": [ + { + "text": "Overall", + "path": "UI/AspNetCore/Overall.md" + }, { "text": "Navigation / Menus", "path": "UI/AspNetCore/Navigation-Menu.md" @@ -418,10 +471,18 @@ "text": "Modals", "path": "UI/AspNetCore/Modals.md" }, + { + "text": "Data Tables", + "path": "UI/AspNetCore/Data-Tables.md" + }, { "text": "Page Alerts", "path": "UI/AspNetCore/Page-Alerts.md" }, + { + "text": "Dynamic JavaScript API Client Proxies", + "path": "UI/AspNetCore/Dynamic-JavaScript-Proxies.md" + }, { "text": "Client Side Package Management", "path": "UI/AspNetCore/Client-Side-Package-Management.md" @@ -448,13 +509,35 @@ "text": "Widgets", "path": "UI/AspNetCore/Widgets.md" }, + { + "text": "Toolbars", + "path": "UI/AspNetCore/Toolbars.md" + }, + { + "text": "Page Header", + "path": "UI/AspNetCore/Page-Header.md" + }, + { + "text": "Branding", + "path": "UI/AspNetCore/Branding.md" + }, { "text": "Layout Hooks", "path": "UI/AspNetCore/Layout-Hooks.md" }, + { + "text": "Testing", + "path": "UI/AspNetCore/Testing.md" + }, { "text": "Theming", - "path": "UI/AspNetCore/Theming.md" + "path": "UI/AspNetCore/Theming.md", + "items": [ + { + "text": "The Basic Theme", + "path": "UI/AspNetCore/Basic-Theme.md" + } + ] }, { "text": "JavaScript API", @@ -531,23 +614,114 @@ "path": "UI/Blazor/Overall.md" }, { - "text": "Services", + "text": "Navigation / Menu", + "path": "UI/Blazor/Navigation-Menu.md" + }, + { + "text": "Localization", + "path": "UI/Blazor/Localization.md" + }, + { + "text": "Theming", "items": [ { "text": "Overall", - "path": "UI/Blazor/Services/Notification.md" + "path": "UI/Blazor/Theming.md" + }, + { + "text": "The Basic Theme", + "path": "UI/Blazor/Basic-Theme.md" + }, + { + "text": "Branding", + "path": "UI/Blazor/Branding.md" + }, + { + "text": "Page Header", + "path": "UI/Blazor/Page-Header.md" + }, + { + "text": "Toolbars", + "path": "UI/Blazor/Toolbars.md" + } + ] + }, + { + "text": "Security", + "items": [ + { + "text": "Authentication", + "path": "UI/Blazor/Authentication.md" + }, + { + "text": "Authorization", + "path": "UI/Blazor/Authorization.md" + } + ] + }, + { + "text": "Services", + "items": [ + { + "text": "Current User", + "path": "UI/Blazor/CurrentUser.md" + }, + { + "text": "Current Tenant", + "path": "UI/Blazor/CurrentTenant.md" + }, + { + "text": "Notification", + "path": "UI/Blazor/Notification.md" + }, + { + "text": "Message", + "path": "UI/Blazor/Message.md" + }, + { + "text": "Page Alerts", + "path": "UI/Blazor/Page-Alerts.md" + }, + { + "text": "Page Progress", + "path": "UI/Blazor/Page-Progress.md" + } + ] + }, + { + "text": "Other Components", + "items": [ + { + "text": "SubmitButton", + "path": "UI/Blazor/Components/SubmitButton.md" } ] + }, + { + "text": "Settings", + "path": "UI/Blazor/Settings.md" + }, + { + "text": "Error Handling", + "path": "UI/Blazor/Error-Handling.md" + }, + { + "text": "Customization / Overriding Components", + "path": "UI/Blazor/Customization-Overriding-Components.md" + }, + { + "text": "Global Scripts & Styles", + "path": "UI/Blazor/Global-Scripts-Styles.md" + }, + { + "text": "Routing", + "path": "UI/Blazor/Routing.md" } ] }, { "text": "Angular", "items": [ - { - "text": "Migration Guide v2.x to v3", - "path": "UI/Angular/Migration-Guide-v3.md" - }, { "text": "Quick Start", "path": "UI/Angular/Quick-Start.md" @@ -570,6 +744,10 @@ { "text": "PWA Configuration", "path": "UI/Angular/PWA-Configuration.md" + }, + { + "text": "Unit Testing", + "path": "UI/Angular/Testing.md" } ] }, @@ -577,8 +755,12 @@ "text": "Core Functionality", "items": [ { - "text": "Config State", - "path": "UI/Angular/Config-State.md" + "text": "Config State Service", + "path": "UI/Angular/Config-State-Service.md" + }, + { + "text": "Authorization", + "path": "UI/Angular/Authorization.md" }, { "text": "HTTP Requests", @@ -588,6 +770,10 @@ "text": "Localization", "path": "UI/Angular/Localization.md" }, + { + "text": "Form Validation", + "path": "UI/Angular/Form-Validation.md" + }, { "text": "Settings", "path": "UI/Angular/Settings.md" @@ -640,6 +826,10 @@ { "text": "Toast Overlay", "path": "UI/Angular/Toaster-Service.md" + }, + { + "text": "Page Alerts", + "path": "UI/Angular/Page-Alerts.md" } ] }, @@ -695,57 +885,44 @@ "path": "Data-Access.md" }, { - "text": "Connection Strings", - "path": "Connection-Strings.md" - }, - { - "text": "Database Providers", + "text": "Entity Framework Core", + "path": "Entity-Framework-Core.md", "items": [ { - "text": "Entity Framework Core", - "path": "Entity-Framework-Core.md", + "text": "Database Migrations", + "path": "Entity-Framework-Core-Migrations.md" + }, + { + "text": "Switch DBMS", + "path": "Entity-Framework-Core-Other-DBMS.md", "items": [ { - "text": "Database Migrations", - "path": "Entity-Framework-Core-Migrations.md" - }, - { - "text": "Switch DBMS", - "path": "Entity-Framework-Core-Other-DBMS.md", - "items": [ - { - "text": "To MySQL", - "path": "Entity-Framework-Core-MySQL.md" - }, - { - "text": "To PostgreSQL", - "path": "Entity-Framework-Core-PostgreSQL.md" - }, - { - "text": "To Oracle", - "path": "Entity-Framework-Core-Oracle.md" - }, - { - "text": "To SQLite", - "path": "Entity-Framework-Core-SQLite.md" - } - ] + "text": "To MySQL", + "path": "Entity-Framework-Core-MySQL.md" + }, + { + "text": "To PostgreSQL", + "path": "Entity-Framework-Core-PostgreSQL.md" + }, + { + "text": "To Oracle", + "path": "Entity-Framework-Core-Oracle.md" + }, + { + "text": "To SQLite", + "path": "Entity-Framework-Core-SQLite.md" } ] - }, - { - "text": "MongoDB", - "path": "MongoDB.md" - }, - { - "text": "Dapper", - "path": "Dapper.md" } ] }, { - "text": "Data Seeding", - "path": "Data-Seeding.md" + "text": "MongoDB", + "path": "MongoDB.md" + }, + { + "text": "Dapper", + "path": "Dapper.md" } ] }, @@ -759,111 +936,136 @@ ] }, { - "text": "Background", + "text": "Testing", + "path": "Testing.md" + }, + { + "text": "Samples", "items": [ { - "text": "Background Jobs", - "path": "Background-Jobs.md", - "items": [ - { - "text": "Hangfire Integration", - "path": "Background-Jobs-Hangfire.md" - }, - { - "text": "RabbitMQ Integration", - "path": "Background-Jobs-RabbitMq.md" - }, - { - "text": "Quartz Integration", - "path": "Background-Jobs-Quartz.md" - } - ] + "text": "All Samples", + "path": "Samples/Index.md" }, { - "text": "Background Workers", - "path": "Background-Workers.md", - "items": [ - { - "text": "Quartz Integration", - "path": "Background-Workers-Quartz.md" - } - ] + "text": "Microservice Demo", + "path": "Samples/Microservice-Demo.md" } ] }, { - "text": "Startup Templates", + "text": "Application Modules", "items": [ { "text": "Overall", - "path": "Startup-Templates/Index.md" + "path": "Modules/Index.md" }, { - "text": "Application", - "path": "Startup-Templates/Application.md" + "text": "Account", + "path": "Modules/Account.md" }, { - "text": "Module", - "path": "Startup-Templates/Module.md" + "text": "Audit Logging", + "path": "Modules/Audit-Logging.md" }, { - "text": "Console", - "path": "Startup-Templates/Console.md" + "text": "Background Jobs", + "path": "Modules/Background-Jobs.md" }, { - "text": "WPF", - "path": "Startup-Templates/WPF.md" + "text": "Blogging", + "path": "Modules/Blogging.md" + }, + { + "text": "Client Simulation", + "path": "Modules/Client-Simulation.md" + }, + { + "text": "CMS Kit", + "path": "Modules/Cms-Kit.md" + }, + { + "text": "Docs", + "path": "Modules/Docs.md" + }, + { + "text": "Feature Management", + "path": "Modules/Feature-Management.md" + }, + { + "text": "Identity", + "path": "Modules/Identity.md" + }, + { + "text": "IdentityServer", + "path": "Modules/IdentityServer.md" + }, + { + "text": "Permission Management", + "path": "Modules/Permission-Management.md" + }, + { + "text": "Setting Management", + "path": "Modules/Setting-Management.md" + }, + { + "text": "Tenant Management", + "path": "Modules/Tenant-Management.md" + }, + { + "text": "Users", + "path": "Modules/Users.md" + }, + { + "text": "Virtual File Explorer", + "path": "Modules/Virtual-File-Explorer.md" } ] }, { - "text": "Samples", + "text": "Release Information", "items": [ { - "text": "All Samples", - "path": "Samples/Index.md" + "text": "Upgrading", + "path": "Upgrading.md" }, { - "text": "Microservice Demo", - "path": "Samples/Microservice-Demo.md" + "text": "Official Packages", + "path": "https://abp.io/packages" + }, + { + "text": "Preview Releases", + "path": "Previews.md" + }, + { + "text": "Nightly Builds", + "path": "Nightly-Builds.md" + }, + { + "text": "Road Map", + "path": "Road-Map.md" + }, + { + "text": "Migration Guides", + "path": "Migration-Guides/Index.md" } ] }, { - "text": "Application Modules", - "path": "Modules/Index.md" - }, - { - "text": "Microservice Architecture", - "path": "Microservice-Architecture.md" - }, - { - "text": "Preview Releases", - "path": "Previews.md" - }, - { - "text": "Nightly Builds", - "path": "Nightly-Builds.md" - }, - { - "text": "Road Map", - "path": "Road-Map.md" - }, - { - "text": "Upgrading", - "path": "Upgrading.md" + "text": "Reference", + "items": [ + { + "text": "CLI", + "path": "CLI.md" + }, + { + "text": "API Documentation", + "path": "{ApiDocumentationUrl}" + } + ] }, { "text": "Contribution Guide", "path": "Contribution/Index.md" - }, - { - "text": "API Documentation", - "path": "{ApiDocumentationUrl}" - }, - { - "text": "Official Packages", - "path": "https://abp.io/packages" } ] } diff --git a/docs/en/images/add-new-propert-to-user-database-extra-properties.png b/docs/en/images/add-new-propert-to-user-database-extra-properties.png new file mode 100644 index 0000000000..90c697461e Binary files /dev/null and b/docs/en/images/add-new-propert-to-user-database-extra-properties.png differ diff --git a/docs/en/images/add-new-propert-to-user-database-field.png b/docs/en/images/add-new-propert-to-user-database-field.png new file mode 100644 index 0000000000..334ba0aa35 Binary files /dev/null and b/docs/en/images/add-new-propert-to-user-database-field.png differ diff --git a/docs/en/images/add-new-property-enum.png b/docs/en/images/add-new-property-enum.png new file mode 100644 index 0000000000..34a14b1bfb Binary files /dev/null and b/docs/en/images/add-new-property-enum.png differ diff --git a/docs/en/images/add-new-property-to-user-form-validation-error-custom.png b/docs/en/images/add-new-property-to-user-form-validation-error-custom.png new file mode 100644 index 0000000000..6bb1799e19 Binary files /dev/null and b/docs/en/images/add-new-property-to-user-form-validation-error-custom.png differ diff --git a/docs/en/images/add-new-property-to-user-form-validation-error.png b/docs/en/images/add-new-property-to-user-form-validation-error.png new file mode 100644 index 0000000000..dd2eb8ad1d Binary files /dev/null and b/docs/en/images/add-new-property-to-user-form-validation-error.png differ diff --git a/docs/en/images/add-new-property-to-user-form.png b/docs/en/images/add-new-property-to-user-form.png new file mode 100644 index 0000000000..bc12a6d5ad Binary files /dev/null and b/docs/en/images/add-new-property-to-user-form.png differ diff --git a/docs/en/images/add-new-property-to-user-table.png b/docs/en/images/add-new-property-to-user-table.png new file mode 100644 index 0000000000..a2a15087cc Binary files /dev/null and b/docs/en/images/add-new-property-to-user-table.png differ diff --git a/docs/en/images/aspnetcore-web-tests-in-solution.png b/docs/en/images/aspnetcore-web-tests-in-solution.png new file mode 100644 index 0000000000..12fca58139 Binary files /dev/null and b/docs/en/images/aspnetcore-web-tests-in-solution.png differ diff --git a/docs/en/images/basic-theme-application-layout-blazor.png b/docs/en/images/basic-theme-application-layout-blazor.png new file mode 100644 index 0000000000..f01becad07 Binary files /dev/null and b/docs/en/images/basic-theme-application-layout-blazor.png differ diff --git a/docs/en/images/basic-theme-application-layout-parts.png b/docs/en/images/basic-theme-application-layout-parts.png new file mode 100644 index 0000000000..5e8f9147a9 Binary files /dev/null and b/docs/en/images/basic-theme-application-layout-parts.png differ diff --git a/docs/en/images/blazor-generic-exception-message.png b/docs/en/images/blazor-generic-exception-message.png new file mode 100644 index 0000000000..5e128003cf Binary files /dev/null and b/docs/en/images/blazor-generic-exception-message.png differ diff --git a/docs/en/images/blazor-message-confirm.png b/docs/en/images/blazor-message-confirm.png new file mode 100644 index 0000000000..fe03620283 Binary files /dev/null and b/docs/en/images/blazor-message-confirm.png differ diff --git a/docs/en/images/blazor-message-error.png b/docs/en/images/blazor-message-error.png new file mode 100644 index 0000000000..5192eae1e9 Binary files /dev/null and b/docs/en/images/blazor-message-error.png differ diff --git a/docs/en/images/blazor-message-success.png b/docs/en/images/blazor-message-success.png new file mode 100644 index 0000000000..592fac8b7d Binary files /dev/null and b/docs/en/images/blazor-message-success.png differ diff --git a/docs/en/images/blazor-notification-bell-component.png b/docs/en/images/blazor-notification-bell-component.png new file mode 100644 index 0000000000..65934ec24b Binary files /dev/null and b/docs/en/images/blazor-notification-bell-component.png differ diff --git a/docs/en/images/blazor-notification-success.png b/docs/en/images/blazor-notification-success.png new file mode 100644 index 0000000000..d68dcf47d8 Binary files /dev/null and b/docs/en/images/blazor-notification-success.png differ diff --git a/docs/en/images/blazor-page-alert-example.png b/docs/en/images/blazor-page-alert-example.png new file mode 100644 index 0000000000..799826e57f Binary files /dev/null and b/docs/en/images/blazor-page-alert-example.png differ diff --git a/docs/en/images/blazor-user-friendly-exception.png b/docs/en/images/blazor-user-friendly-exception.png new file mode 100644 index 0000000000..fa2a70d931 Binary files /dev/null and b/docs/en/images/blazor-user-friendly-exception.png differ diff --git a/docs/en/images/bookstore-branding-blazor.png b/docs/en/images/bookstore-branding-blazor.png new file mode 100644 index 0000000000..74ddf35f0f Binary files /dev/null and b/docs/en/images/bookstore-branding-blazor.png differ diff --git a/docs/en/images/bookstore-logo-blazor.png b/docs/en/images/bookstore-logo-blazor.png new file mode 100644 index 0000000000..4e01569813 Binary files /dev/null and b/docs/en/images/bookstore-logo-blazor.png differ diff --git a/docs/en/images/branding-appname.png b/docs/en/images/branding-appname.png new file mode 100644 index 0000000000..9300ad2c7f Binary files /dev/null and b/docs/en/images/branding-appname.png differ diff --git a/docs/en/images/branding-nobrand.png b/docs/en/images/branding-nobrand.png new file mode 100644 index 0000000000..1fdf78472b Binary files /dev/null and b/docs/en/images/branding-nobrand.png differ diff --git a/docs/en/images/breadcrumbs-example.png b/docs/en/images/breadcrumbs-example.png new file mode 100644 index 0000000000..17ae5f8ba5 Binary files /dev/null and b/docs/en/images/breadcrumbs-example.png differ diff --git a/docs/en/images/datatables-custom-render-date.png b/docs/en/images/datatables-custom-render-date.png new file mode 100644 index 0000000000..344237aea7 Binary files /dev/null and b/docs/en/images/datatables-custom-render-date.png differ diff --git a/docs/en/images/datatables-default-render-date.png b/docs/en/images/datatables-default-render-date.png new file mode 100644 index 0000000000..430c889d2f Binary files /dev/null and b/docs/en/images/datatables-default-render-date.png differ diff --git a/docs/en/images/datatables-example.png b/docs/en/images/datatables-example.png new file mode 100644 index 0000000000..ef3e74576b Binary files /dev/null and b/docs/en/images/datatables-example.png differ diff --git a/docs/en/images/datatables-row-actions-confirmation.png b/docs/en/images/datatables-row-actions-confirmation.png new file mode 100644 index 0000000000..9be8433f9c Binary files /dev/null and b/docs/en/images/datatables-row-actions-confirmation.png differ diff --git a/docs/en/images/datatables-row-actions-icon.png b/docs/en/images/datatables-row-actions-icon.png new file mode 100644 index 0000000000..70111fc1ac Binary files /dev/null and b/docs/en/images/datatables-row-actions-icon.png differ diff --git a/docs/en/images/datatables-row-actions.png b/docs/en/images/datatables-row-actions.png new file mode 100644 index 0000000000..4eae0f098a Binary files /dev/null and b/docs/en/images/datatables-row-actions.png differ diff --git a/docs/en/images/db-options.png b/docs/en/images/db-options.png new file mode 100644 index 0000000000..d9dc2ed0f4 Binary files /dev/null and b/docs/en/images/db-options.png differ diff --git a/docs/en/images/ddd-microservice-simple.png b/docs/en/images/ddd-microservice-simple.png new file mode 100644 index 0000000000..1217b243c8 Binary files /dev/null and b/docs/en/images/ddd-microservice-simple.png differ diff --git a/docs/en/images/domain-driven-design-aggregate-keep-small.png b/docs/en/images/domain-driven-design-aggregate-keep-small.png new file mode 100644 index 0000000000..56f805ebbe Binary files /dev/null and b/docs/en/images/domain-driven-design-aggregate-keep-small.png differ diff --git a/docs/en/images/domain-driven-design-clean-architecture.png b/docs/en/images/domain-driven-design-clean-architecture.png new file mode 100644 index 0000000000..4ab6d2c93b Binary files /dev/null and b/docs/en/images/domain-driven-design-clean-architecture.png differ diff --git a/docs/en/images/domain-driven-design-domain-vs-application-logic.png b/docs/en/images/domain-driven-design-domain-vs-application-logic.png new file mode 100644 index 0000000000..e370cba71e Binary files /dev/null and b/docs/en/images/domain-driven-design-domain-vs-application-logic.png differ diff --git a/docs/en/images/domain-driven-design-entity-primary-keys.png b/docs/en/images/domain-driven-design-entity-primary-keys.png new file mode 100644 index 0000000000..7ee0096222 Binary files /dev/null and b/docs/en/images/domain-driven-design-entity-primary-keys.png differ diff --git a/docs/en/images/domain-driven-design-example-domain-schema.png b/docs/en/images/domain-driven-design-example-domain-schema.png new file mode 100644 index 0000000000..fe3075b141 Binary files /dev/null and b/docs/en/images/domain-driven-design-example-domain-schema.png differ diff --git a/docs/en/images/domain-driven-design-issue-aggregate-diagram.png b/docs/en/images/domain-driven-design-issue-aggregate-diagram.png new file mode 100644 index 0000000000..244564126a Binary files /dev/null and b/docs/en/images/domain-driven-design-issue-aggregate-diagram.png differ diff --git a/docs/en/images/domain-driven-design-layers.png b/docs/en/images/domain-driven-design-layers.png new file mode 100644 index 0000000000..f2fc9af097 Binary files /dev/null and b/docs/en/images/domain-driven-design-layers.png differ diff --git a/docs/en/images/domain-driven-design-multiple-applications.png b/docs/en/images/domain-driven-design-multiple-applications.png new file mode 100644 index 0000000000..6db4fc6443 Binary files /dev/null and b/docs/en/images/domain-driven-design-multiple-applications.png differ diff --git a/docs/en/images/domain-driven-design-project-relations.png b/docs/en/images/domain-driven-design-project-relations.png new file mode 100644 index 0000000000..b7878d1385 Binary files /dev/null and b/docs/en/images/domain-driven-design-project-relations.png differ diff --git a/docs/en/images/domain-driven-design-reference-by-id-sample.png b/docs/en/images/domain-driven-design-reference-by-id-sample.png new file mode 100644 index 0000000000..48a1913654 Binary files /dev/null and b/docs/en/images/domain-driven-design-reference-by-id-sample.png differ diff --git a/docs/en/images/domain-driven-design-vs-solution.png b/docs/en/images/domain-driven-design-vs-solution.png new file mode 100644 index 0000000000..ef0f51a418 Binary files /dev/null and b/docs/en/images/domain-driven-design-vs-solution.png differ diff --git a/docs/en/images/domain-driven-design-web-request-flow.png b/docs/en/images/domain-driven-design-web-request-flow.png new file mode 100644 index 0000000000..a9cb98e01f Binary files /dev/null and b/docs/en/images/domain-driven-design-web-request-flow.png differ diff --git a/docs/en/images/example-global-styles.png b/docs/en/images/example-global-styles.png new file mode 100644 index 0000000000..04232aa62d Binary files /dev/null and b/docs/en/images/example-global-styles.png differ diff --git a/docs/en/images/extension-navigation-property-form.png b/docs/en/images/extension-navigation-property-form.png new file mode 100644 index 0000000000..5e55ce3afd Binary files /dev/null and b/docs/en/images/extension-navigation-property-form.png differ diff --git a/docs/en/images/extension-navigation-property-table.png b/docs/en/images/extension-navigation-property-table.png new file mode 100644 index 0000000000..a3f333919c Binary files /dev/null and b/docs/en/images/extension-navigation-property-table.png differ diff --git a/docs/en/images/issue-first-test.png b/docs/en/images/issue-first-test.png new file mode 100644 index 0000000000..b2be1aa8eb Binary files /dev/null and b/docs/en/images/issue-first-test.png differ diff --git a/docs/en/images/issue-list.png b/docs/en/images/issue-list.png new file mode 100644 index 0000000000..96e9010ed6 Binary files /dev/null and b/docs/en/images/issue-list.png differ diff --git a/docs/en/images/lepton-theme-blazor-layout.png b/docs/en/images/lepton-theme-blazor-layout.png new file mode 100644 index 0000000000..e4211b367e Binary files /dev/null and b/docs/en/images/lepton-theme-blazor-layout.png differ diff --git a/docs/en/images/page-toolbar-button.png b/docs/en/images/page-toolbar-button.png new file mode 100644 index 0000000000..05e3f9da11 Binary files /dev/null and b/docs/en/images/page-toolbar-button.png differ diff --git a/docs/en/images/page-toolbar-custom-component.png b/docs/en/images/page-toolbar-custom-component.png new file mode 100644 index 0000000000..a15ef4854c Binary files /dev/null and b/docs/en/images/page-toolbar-custom-component.png differ diff --git a/docs/en/images/simple-plug-in-dll-file.png b/docs/en/images/simple-plug-in-dll-file.png new file mode 100644 index 0000000000..3155708d68 Binary files /dev/null and b/docs/en/images/simple-plug-in-dll-file.png differ diff --git a/docs/en/images/simple-plugin-library.png b/docs/en/images/simple-plugin-library.png new file mode 100644 index 0000000000..9fefd57dda Binary files /dev/null and b/docs/en/images/simple-plugin-library.png differ diff --git a/docs/en/images/simple-plugin-output.png b/docs/en/images/simple-plugin-output.png new file mode 100644 index 0000000000..71f6a78c0e Binary files /dev/null and b/docs/en/images/simple-plugin-output.png differ diff --git a/docs/en/images/simple-razor-plug-in-dll-file.png b/docs/en/images/simple-razor-plug-in-dll-file.png new file mode 100644 index 0000000000..06b7a565fe Binary files /dev/null and b/docs/en/images/simple-razor-plug-in-dll-file.png differ diff --git a/docs/en/images/simple-razor-plugin.png b/docs/en/images/simple-razor-plugin.png new file mode 100644 index 0000000000..92e0e00d29 Binary files /dev/null and b/docs/en/images/simple-razor-plugin.png differ diff --git a/docs/en/images/solution-test-projects.png b/docs/en/images/solution-test-projects.png new file mode 100644 index 0000000000..e51c368e9f Binary files /dev/null and b/docs/en/images/solution-test-projects.png differ diff --git a/docs/en/images/table-column-extension-example.png b/docs/en/images/table-column-extension-example.png new file mode 100644 index 0000000000..84d87bf46c Binary files /dev/null and b/docs/en/images/table-column-extension-example.png differ diff --git a/docs/en/images/ui-options.png b/docs/en/images/ui-options.png new file mode 100644 index 0000000000..bd211a8914 Binary files /dev/null and b/docs/en/images/ui-options.png differ diff --git a/docs/en/images/user-action-extension-click-me.png b/docs/en/images/user-action-extension-click-me.png new file mode 100644 index 0000000000..4046630f48 Binary files /dev/null and b/docs/en/images/user-action-extension-click-me.png differ diff --git a/docs/en/images/user-action-extension-on-solution.png b/docs/en/images/user-action-extension-on-solution.png new file mode 100644 index 0000000000..5f58d3f302 Binary files /dev/null and b/docs/en/images/user-action-extension-on-solution.png differ diff --git a/docs/en/images/vs-run-tests-in-parallel.png b/docs/en/images/vs-run-tests-in-parallel.png new file mode 100644 index 0000000000..765cba5100 Binary files /dev/null and b/docs/en/images/vs-run-tests-in-parallel.png differ diff --git a/docs/en/images/vs-startup-template-tests.png b/docs/en/images/vs-startup-template-tests.png new file mode 100644 index 0000000000..f16d79a4c7 Binary files /dev/null and b/docs/en/images/vs-startup-template-tests.png differ diff --git a/docs/en/images/vs-test-explorer.png b/docs/en/images/vs-test-explorer.png new file mode 100644 index 0000000000..e225ee5e92 Binary files /dev/null and b/docs/en/images/vs-test-explorer.png differ diff --git a/docs/zh-Hans/Application-Services.md b/docs/zh-Hans/Application-Services.md index 6bb18daa37..e68217a94d 100644 --- a/docs/zh-Hans/Application-Services.md +++ b/docs/zh-Hans/Application-Services.md @@ -353,12 +353,12 @@ public class DistrictAppService { } - protected override async Task DeleteByIdAsync(DistrictKey id) + protected async override Task DeleteByIdAsync(DistrictKey id) { await Repository.DeleteAsync(d => d.CityId == id.CityId && d.Name == id.Name); } - protected override async Task GetEntityByIdAsync(DistrictKey id) + protected async override Task GetEntityByIdAsync(DistrictKey id) { return await AsyncQueryableExecuter.FirstOrDefaultAsync( Repository.Where(d => d.CityId == id.CityId && d.Name == id.Name) diff --git a/docs/zh-Hans/Authentication/Social-External-Logins.md b/docs/zh-Hans/Authentication/Social-External-Logins.md index 55ab97374c..a1cdbb86ce 100644 --- a/docs/zh-Hans/Authentication/Social-External-Logins.md +++ b/docs/zh-Hans/Authentication/Social-External-Logins.md @@ -1,33 +1,3 @@ # 社交/外部登录 - -[帐户模块](../Modules/Account.md)已配置为开箱即用的处理社交或外部登录. 你可以按照ASP.NET Core文档向你的应用程序添加社交/外部登录提供程序. - -## 示例: Facebook 认证 - -按照[ASP.NET Core Facebook集成文档](https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/social/facebook-logins)向你应用程序添加Facebook登录. - -#### 添加NuGet包 - -添加[Microsoft.AspNetCore.Authentication.Facebook]包到你的项目. 基于你的架构,可能是 `.Web`,`.IdentityServer`(对于分层启动)或 `.Host` 项目. - -#### 配置提供程序 - -在你模块的 `ConfigureServices` 方法中使用 `.AddFacebook(...)` 扩展方法来配置客户端: - -````csharp -context.Services.AddAuthentication() - .AddFacebook(facebook => - { - facebook.AppId = "..."; - facebook.AppSecret = "..."; - facebook.Scope.Add("email"); - facebook.Scope.Add("public_profile"); - }); -```` - -> 最佳实践是使用 `appsettings.json` 或ASP.NET Core用户机密系统来存储你的凭据,而不是像这样硬编码值. 请参阅[微软](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins)文档了解如何使用用户机密. - -## Angular UI - -从v3.1开始,Angular UI使用授权码流程(作为最佳实践)通过重定向到MVC UI登录页面来对用户进行身份验证. 因此,即使你使用的是Angular UI,社交/外部登录集成也与上面说明的相同.并且可以开箱即用. \ No newline at end of file +> 文档已经移动其他位置. 参阅[账户模块](../Modules/Account.md)文档. \ No newline at end of file diff --git a/docs/zh-Hans/Authorization.md b/docs/zh-Hans/Authorization.md index 96caae1404..db0dfdb654 100644 --- a/docs/zh-Hans/Authorization.md +++ b/docs/zh-Hans/Authorization.md @@ -343,7 +343,7 @@ public class SystemAdminPermissionValueProvider : PermissionValueProvider public override string Name => "SystemAdmin"; - public override async Task + public async override Task CheckAsync(PermissionValueCheckContext context) { if (context.Principal?.FindFirst("User_Type")?.Value == "SystemAdmin") diff --git a/docs/zh-Hans/Background-Workers.md b/docs/zh-Hans/Background-Workers.md index 1daedde384..26bafed2da 100644 --- a/docs/zh-Hans/Background-Workers.md +++ b/docs/zh-Hans/Background-Workers.md @@ -54,7 +54,7 @@ public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase Timer.Period = 600000; //10 minutes } - protected override async Task DoWorkAsync( + protected async override Task DoWorkAsync( PeriodicBackgroundWorkerContext workerContext) { Logger.LogInformation("Starting: Setting status of inactive users..."); diff --git a/docs/zh-Hans/Blob-Storing-Aliyun.md b/docs/zh-Hans/Blob-Storing-Aliyun.md index ded28fbc93..c8a1e68bc7 100644 --- a/docs/zh-Hans/Blob-Storing-Aliyun.md +++ b/docs/zh-Hans/Blob-Storing-Aliyun.md @@ -55,7 +55,7 @@ Configure(options => * **RoleSessionName** ([NotNull]string): 用来标识临时访问凭证的名称,建议使用不同的应用程序用户来区分. * **Policy** (string): 在扮演角色的时候额外添加的权限限制. 请参见[基于RAM Policy的权限控制](https://help.aliyun.com/document_detail/100680.html). * **DurationSeconds** (int): 设置临时访问凭证的有效期,单位是s,最小为900,最大为3600. -* **ContainerName** (string): 你可以在aliyun中指定容器名称. 如果没有指定它将使用 `BlogContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aliyun有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://help.aliyun.com/knowledge_detail/39668.html): +* **ContainerName** (string): 你可以在aliyun中指定容器名称. 如果没有指定它将使用 `BlobContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aliyun有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://help.aliyun.com/knowledge_detail/39668.html): * 只能包含小写字母,数字和短横线(-) * 必须以小写字母和数字开头和结尾 * Bucket名称的长度限制在**3**到**63**个字符之间 diff --git a/docs/zh-Hans/Blob-Storing-Aws.md b/docs/zh-Hans/Blob-Storing-Aws.md index 0ecb59bbf5..7bfb998297 100644 --- a/docs/zh-Hans/Blob-Storing-Aws.md +++ b/docs/zh-Hans/Blob-Storing-Aws.md @@ -59,7 +59,7 @@ Configure(options => * **Region** (string): 服务的地区名称. * **Policy** (string): JSON格式的IAM策略. * **DurationSeconds** (int): 设置临时访问凭证的有效期,单位是s,最小为900,最大为129600. -* **ContainerName** (string): 你可以在Aws中指定容器名称. 如果没有指定它将使用 `BlogContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aws有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): +* **ContainerName** (string): 你可以在Aws中指定容器名称. 如果没有指定它将使用 `BlobContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aws有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): * Bucket名称必须介于 3 到 63 个字符之间. * Bucket名称只能由小写字母、数字、句点 (.) 和连字符 (-) 组成. * Bucket名称必须以字母或数字开头和结尾. diff --git a/docs/zh-Hans/Blob-Storing-Azure.md b/docs/zh-Hans/Blob-Storing-Azure.md index b6f54cd72c..8eecc1fde3 100644 --- a/docs/zh-Hans/Blob-Storing-Azure.md +++ b/docs/zh-Hans/Blob-Storing-Azure.md @@ -40,7 +40,7 @@ Configure(options => ### 选项 * **ConnectionString** (string): 连接字符串包括应用程序在运行时使用共享密钥授权访问Azure存储帐户中的数据所需的授权信息. 请参考[Azure文档](https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string). -* **ContainerName** (string): 你可以在azure中指定容器名称. 如果没有指定它将使用 `BlogContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Azure有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names): +* **ContainerName** (string): 你可以在azure中指定容器名称. 如果没有指定它将使用 `BlobContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Azure有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names): * 容器名称必须以字母或数字开头或结尾,并且只能包含字母,数字和破折号(-)字符. * 每个破折号(-)必须紧跟在字母或数字之后;容器名称中不允许使用连续的破折号. * 容器名称中的所有字母都必须**小写**. diff --git a/docs/zh-Hans/Blob-Storing-Minio.md b/docs/zh-Hans/Blob-Storing-Minio.md index 3c4bec721d..fb98281f02 100644 --- a/docs/zh-Hans/Blob-Storing-Minio.md +++ b/docs/zh-Hans/Blob-Storing-Minio.md @@ -43,7 +43,7 @@ Configure(options => * **EndPoint** (string): 你的Minio对象存储服务的URL, 查看文档:https://docs.min.io/docs/dotnet-client-quickstart-guide.html * **AccessKey** (string): Access key是唯一标识你的账户的用户ID, * **SecretKey** (string): Access key是唯一标识你的账户的用户ID -* **BucketName** (string):你可以指定bucket名称,如果没有指定,将使用 `BlogContainerName` 属性定义的BLOB容器的名称(查阅[BLOB storing document](Blob-Storing.md)),MinIO完全兼容S3标准,所以有一些 **bucket命名规则**,必须符合[规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): +* **BucketName** (string):你可以指定bucket名称,如果没有指定,将使用 `BlobContainerName` 属性定义的BLOB容器的名称(查阅[BLOB storing document](Blob-Storing.md)),MinIO完全兼容S3标准,所以有一些 **bucket命名规则**,必须符合[规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): * Bucket名称必须介于 3 到 63 个字符之间. * Bucket名称只能由小写字母、数字、句点 (.) 和连字符 (-) 组成. * Bucket名称必须以字母或数字开头和结尾. diff --git a/docs/zh-Hans/Blob-Storing.md b/docs/zh-Hans/Blob-Storing.md index 99688ca47a..9321c51cb9 100644 --- a/docs/zh-Hans/Blob-Storing.md +++ b/docs/zh-Hans/Blob-Storing.md @@ -10,7 +10,7 @@ ABP框架为BLOB提供了抽象,并提供了一些可以轻松集成到的预构 * 你可以**轻松的更改**BLOB存储,而不用改变你的应用程序代码. * 如果你想创建**可重用的应用程序模块**,无需假设BLOB的存储方式. -ABP BLOG存储系统兼容ABP框架其他功能,如[多租户](Multi-Tenancy.md). +ABP BLOB存储系统兼容ABP框架其他功能,如[多租户](Multi-Tenancy.md). ## BLOB 存储提供程序 @@ -18,10 +18,10 @@ ABP框架已经有以下存储提供程序的实现; * [File System](Blob-Storing-File-System.md):将BLOB作为标准文件存储在本地文件系统的文件夹中. * [Database](Blob-Storing-Database.md): 将BLOB存储在数据库中. -* [Azure](Blob-Storing-Azure.md): 将BLOG存储在 [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/)中. +* [Azure](Blob-Storing-Azure.md): 将BLOB存储在 [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/)中. * [Aliyun](Blob-Storing-Aliyun.md): 将BLOB存储在[Aliyun Storage Service](https://help.aliyun.com/product/31815.html)中. -* [Ninio](Blob-Storing-Minio.md): 将BLOB存储在[MinIO Object storage](https://min.io/)中. -* [Aws](Blob-Storing-Aws.md): 将BLOB存储在[Amazon Simple Storage Service](https://min.io/)中. +* [Minio](Blob-Storing-Minio.md): 将BLOB存储在[MinIO Object storage](https://min.io/)中. +* [Aws](Blob-Storing-Aws.md): 将BLOB存储在[Amazon Simple Storage Service](https://aws.amazon.com/s3/)中. 以后会实现更多的提供程序,你可以为自己喜欢的提供程序创建[请求](https://github.com/abpframework/abp/issues/new),或者你也可以[自己实现](Blob-Storing-Custom-Provider.md)它并[贡献](Contribution/Index.md)到ABP框架. @@ -95,9 +95,9 @@ namespace AbpDemo ### 读取/获取 BLOB * `GetAsync`: 返回给定BLOB名称可用于读取BLOB内容的 `Stream` 对象. 使用后始终要**dispose流**. 如果找不到具有给定名称的BLOB,则抛出异常. -* `GetOrNullAsync`: 与 `GetAsync` 方法相反,如果未找到给定名称的BLOG,则返回 `null`. +* `GetOrNullAsync`: 与 `GetAsync` 方法相反,如果未找到给定名称的BLOB,则返回 `null`. * `GetAllBytesAsync`: 返回 `byte[]` 而不是 `Stream`. 如果找不到具有给定名称的BLOB,则抛出异常. -* `GetAllBytesOrNullAsync`: 与 `GetAllBytesAsync` 方法相反,如果未找到给定名称的BLOG,则返回 `null`. +* `GetAllBytesOrNullAsync`: 与 `GetAllBytesAsync` 方法相反,如果未找到给定名称的BLOB,则返回 `null`. ### 删除 BLOB @@ -300,10 +300,10 @@ Configure(options => ## BLOB 存储 vs 文件管理系统 -注意BLOB存储不是一个文件管理系统. 它是一个用于保存,获取和删除命名BLOG的低级别系统. 它不提供目录那样的层次结构,这是典型文件系统所期望的. +注意BLOB存储不是一个文件管理系统. 它是一个用于保存,获取和删除命名BLOB的低级别系统. 它不提供目录那样的层次结构,这是典型文件系统所期望的. 如果你想创建文件夹并在文件夹之间移动文件,为文件分配权限并在用户之间共享文件,那么你需要在BLOB存储系统上实现你自己的应用程序. ## 另请参阅 -* [创建自定义BLOB存储提供程序](Blob-Storing-Custom-Provider.md) \ No newline at end of file +* [创建自定义BLOB存储提供程序](Blob-Storing-Custom-Provider.md) diff --git a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/Post.md b/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/Post.md index 0f97391eb8..bc448179cc 100644 --- a/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/Post.md +++ b/docs/zh-Hans/Blog-Posts/2020-05-08 v2_7_Release/Post.md @@ -121,7 +121,7 @@ ABP框架的[异常处理系统](https://docs.abp.io/en/abp/latest/Exception-Han ````csharp public class MyExceptionSubscriber : ExceptionSubscriber { - public override async Task HandleAsync(ExceptionNotificationContext context) + public async override Task HandleAsync(ExceptionNotificationContext context) { //TODO... } diff --git a/docs/zh-Hans/CLI.md b/docs/zh-Hans/CLI.md index 11f9964279..4a02b1c3c8 100644 --- a/docs/zh-Hans/CLI.md +++ b/docs/zh-Hans/CLI.md @@ -34,7 +34,7 @@ dotnet tool update -g Volo.Abp.Cli * **`generate-proxy`**: 生成客户端代理以使用HTTP API端点. * **`remove-proxy`**: 移除以前生成的客户端代理. * **`switch-to-preview`**: 切换到ABP框架的最新预览版本。 -* **`switch-to-preview`**: 切换解决方案所有ABP相关包为[夜间构建](Nightly-Builds.md)版本. +* **`switch-to-nightly`**: 切换解决方案所有ABP相关包为[夜间构建](Nightly-Builds.md)版本. * **`switch-to-stable`**: 切换解决方案所有ABP相关包为最新的稳定版本. * **`translate`**: 当源代码控制存储库中有多个JSON[本地化](Localization.md文件时,可简化翻译本地化文件的过程. * **`login`**: 使用你在[abp.io](https://abp.io/)的用户名和密码在你的计算机上认证. @@ -103,7 +103,7 @@ abp new Acme.BookStore * `--preview`: 使用最新的预览版本. * `--template-source` 或者 `-ts`: 指定自定义模板源用于生成项目,可以使用本地源和网络源(例如 `D:\local-templat` 或 `https://.../my-template-file.zip`). * `--create-solution-folder` 或者 `-csf`: 指定项目是在输出文件夹中的新文件夹中还是直接在输出文件夹中. -* `--connection-string` 或者 `-cs`: 重写所有 `appsettings.json` 文件的默认连接字符串. 默认连接字符串是 `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true`. 默认的数据库提供程序是 `SQL Server`. 如果你使用EF Core但需要更改DBMS,可以按[这里所述](Entity-Framework-Core-Other-DBMS.md)进行更改(创建解决方案之后). +* `--connection-string` 或者 `-cs`: 重写所有 `appsettings.json` 文件的默认连接字符串. 默认连接字符串是 `Server=localhost;Database=MyProjectName;Trusted_Connection=True`. 默认的数据库提供程序是 `SQL Server`. 如果你使用EF Core但需要更改DBMS,可以按[这里所述](Entity-Framework-Core-Other-DBMS.md)进行更改(创建解决方案之后). * `--local-framework-ref --abp-path`: 使用对项目的本地引用,而不是替换为NuGet包引用. ### update @@ -125,7 +125,8 @@ abp update [options] * `--nuget`: 仅更新的NuGet包 * `--solution-path` 或 `-sp`: 指定解决方案路径/目录. 默认使用当前目录 * `--solution-name` 或 `-sn`: 指定解决方案名称. 默认在目录中搜索`*.sln`文件. -*`--check-all`: 分别检查每个包的新版本. 默认是 `false`. +* `--check-all`: 分别检查每个包的新版本. 默认是 `false`. +* `--version` or `-v`: 指定用于升级的版本. 如果没有指定,则使用最新版本. ### add-package diff --git a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md index 21245cec16..b8abe99cdc 100644 --- a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md +++ b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md @@ -59,7 +59,6 @@ context.Services.Replace( ### 示例: 重写服务方法 ````csharp -//[RemoteService(IsEnabled = false)] // 如果你在使用动态控制器,为了避免为应用服务创建重复的控制器, 你可以禁用远程访问. [Dependency(ReplaceServices = true)] [ExposeServices(typeof(IIdentityUserAppService), typeof(IdentityUserAppService), typeof(MyIdentityUserAppService))] public class MyIdentityUserAppService : IdentityUserAppService @@ -76,7 +75,7 @@ public class MyIdentityUserAppService : IdentityUserAppService { } - public override async Task CreateAsync(IdentityUserCreateDto input) + public async override Task CreateAsync(IdentityUserCreateDto input) { if (input.PhoneNumber.IsNullOrWhiteSpace()) { @@ -109,33 +108,33 @@ public class MyIdentityUserManager : IdentityUserManager { public MyIdentityUserManager( IdentityUserStore store, - IIdentityRoleRepository roleRepository, + IIdentityRoleRepository roleRepository, IIdentityUserRepository userRepository, - IOptions optionsAccessor, + IOptions optionsAccessor, IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, + IEnumerable> userValidators, + IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, - ILogger logger, - ICancellationTokenProvider cancellationTokenProvider) : + ILogger logger, + ICancellationTokenProvider cancellationTokenProvider) : base(store, roleRepository, - userRepository, - optionsAccessor, - passwordHasher, - userValidators, + userRepository, + optionsAccessor, + passwordHasher, + userValidators, passwordValidators, - keyNormalizer, - errors, - services, - logger, + keyNormalizer, + errors, + services, + logger, cancellationTokenProvider) { } - public override async Task CreateAsync(IdentityUser user) + public async override Task CreateAsync(IdentityUser user) { if (user.PhoneNumber.IsNullOrWhiteSpace()) { @@ -251,8 +250,8 @@ ObjectExtensionManager.Instance .AddOrUpdateProperty( new[] { - typeof(IdentityUserDto), - typeof(IdentityUserCreateDto), + typeof(IdentityUserDto), + typeof(IdentityUserCreateDto), typeof(IdentityUserUpdateDto) }, "SocialSecurityNumber" diff --git a/docs/zh-Hans/Data-Access.md b/docs/zh-Hans/Data-Access.md index 68baa6dd87..bf3eb479c8 100644 --- a/docs/zh-Hans/Data-Access.md +++ b/docs/zh-Hans/Data-Access.md @@ -8,4 +8,8 @@ ABP框架被设计为与数据库无关, 它通过[仓储](Repositories.md)和[ * [MongoDB](MongoDB.md) * [Dapper](Dapper.md) -在以后的版本中可能会添加更多的提供程序. \ No newline at end of file +## 另请参阅 + +* [连接字符串](Connection-Strings.md) +* [种子数据](Data-Seeding.md) +* [数据过滤](Data-Filtering.md) \ No newline at end of file diff --git a/docs/zh-Hans/Data-Seeding.md b/docs/zh-Hans/Data-Seeding.md index 68f6fc1e73..97e7c7915c 100644 --- a/docs/zh-Hans/Data-Seeding.md +++ b/docs/zh-Hans/Data-Seeding.md @@ -38,31 +38,37 @@ namespace Acme.BookStore { private readonly IRepository _bookRepository; private readonly IGuidGenerator _guidGenerator; + private readonly ICurrentTenant _currentTenant; public BookStoreDataSeedContributor( IRepository bookRepository, - IGuidGenerator guidGenerator) + IGuidGenerator guidGenerator, + ICurrentTenant currentTenant) { _bookRepository = bookRepository; _guidGenerator = guidGenerator; + _currentTenant = currentTenant; } public async Task SeedAsync(DataSeedContext context) { - if (await _bookRepository.GetCountAsync() > 0) + using (_currentTenant.Change(context?.TenantId)) { - return; + if (await _bookRepository.GetCountAsync() > 0) + { + return; + } + + var book = new Book( + id: _guidGenerator.Create(), + name: "The Hitchhiker's Guide to the Galaxy", + type: BookType.ScienceFiction, + publishDate: new DateTime(1979, 10, 12), + price: 42 + ); + + await _bookRepository.InsertAsync(book); } - - var book = new Book( - id: _guidGenerator.Create(), - name: "The Hitchhiker's Guide to the Galaxy", - type: BookType.ScienceFiction, - publishDate: new DateTime(1979, 10, 12), - price: 42 - ); - - await _bookRepository.InsertAsync(book); } } } diff --git a/docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md b/docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md new file mode 100644 index 0000000000..81755fbf0d --- /dev/null +++ b/docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md @@ -0,0 +1,1979 @@ +# 实现领域驱动设计 + +## 总述 + +本文是实现领域驱动设计(DDD)的**实用指南**.虽然在实现中依赖了ABP框架,但是本文中的概念,理论和设计模式同样适用于其它类型的项目,不仅限于.Net项目. + +### 目标 + +本文的目标是: + +* **介绍并解释**DDD的架构,概念,原理及构建. +* **解释**ABP框架的分层架构及解决方案结构. +* 通过**案例**,介绍实现DDD的一些**规则**及最佳实践. +* 展示**ABP框架**为DDD的实现提供了哪些基础设施. +* 最后,基于软件开发**最佳实践**和我们的经验提供**建议**来创建一个**可维护的代码库**. + +### 简单的代码 + +> **踢足球**非常**简单**,但是**踢简单的足球**却**非常难**.— 约翰·克鲁伊夫(Johan Cruyff) + +在编码的世界中,引用此名言: + +> **写代码**非常**简单**,但是**写简单的代码**却**非常难** — ??? + +在本文中,我们将介绍一些容易实现的规则. + +随着**应用程序的变化**,有时候,为了节省开发时间会**违反一些本应遵守的规则**,使得代码变得**复杂**且难以维护.短期来看确实节省了开发时间,但是后期可能需要花费更多的时间为之前的偷懒而**买单**.**无法对原有的代码进行维护**,导致大量的逻辑都需要进行**重写**. + +如果你**遵循规则并按最佳实践的方式**进行编码,那么你的代码将易于维护,你的业务逻辑将**更快的满足**需求的变化. + +## 什么是领域驱动设计? + +领域驱动设计(DDD)是一种将实现与**持续进化**的模型连接在一起来满足**复杂**需求的软件开发方法. + +DDD适用于**复杂领域**或**较大规模**的系统,而不是简单的CRUD程序.它着重与**核心领域逻辑**,而不是基础架构.这样有助于构建一个**灵活**,模块化,**可维护**的代码库. + +### OOP & SOLID + +实现DDD高度依赖面对对象编程思想(OOP)和[SOLID](https://zh.wikipedia.org/wiki/SOLID_(%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1))原则.事实上,DDD已经**实现**并**延伸**了这些原则,因此,**深入了解**OOP和SOLID对实施DDD十分有利. + +### DDD分层与整洁架构 + +基于DDD的架构分为四个基础层 + +![domain-driven-design-layers](images/domain-driven-design-layers.png) + +**业务逻辑**分为两层,分别为 *领域(Domain)* 层和 *应用(Application)* 层,它们包含不同类型的业务逻辑. + +* **领域层**:只实现领域业务逻辑,与用例无关. +* **应用层**:基于领域层来实现满足用例的业务逻辑.用例可以看作是用户界面(UI)或外部应用程序的交互. +* **展现层**:包含应用程序的UI元素. +* **基础设施层**:通过对第三方库的集成或抽象,来满足其它层的非核心业务逻辑的实现. + +同样的分层架构也可以如下图所示:被称为 **整洁架构**, 又或者称为 **洋葱架构**: + +![domain-driven-design-clean-architecture](images/domain-driven-design-clean-architecture.png) + +在整洁架构中,**每层只依赖内部的层**,独立的层在圆圈的最中心,也就是领域层. + +### 核心构建组成 + +DDD的关注点在**领域层**和**应用层**上,而展现层和基础设施层则视为*细节*(这个词原文太抽象,自己体会吧),业务层不应依赖它们. + +这并不意味着展现层和基础设施层不重要.它们非常重要,但*UI框架* 和 *数据库提供程序* 需要你自己定义规则和总结最佳实践.这些不在DDD的讨论范围中. + +本节将介绍领域层和应用层的基本构建组件. + +#### 领域层构建组成 + +* **实体(Entity)**: [实体](Entities.md)是种领域对象,它有自己的属性(状态,数据)和执行业务逻辑的方法.实体由唯一标识符(Id)表示,不同ID的两个实体被视为不同的实体. +* **值对象(Value Object)**: [值对象](Value-Objects.md)是另外一种类型的领域对象,使用值对象的属性来判断两个值对象是否相同,而非使用ID判断.如果两个值对象的属性值全部相同就被视为同一对象.值对象通常是不可变的,大多数情况下它比实体简单. +* **聚合(Aggregate) 和 聚合根(Aggregate Root)**: [聚合](Entities.md)是由**聚合根**包裹在一起的一组对象(实体和值对象).聚合根是一种具有特定职责的实体. +* **仓储(Repository)** (接口): [仓储](Repositories.md)是被领域层或应用层调用的数据库持久化接口.它隐藏了DBMS的复杂性,领域层中只定义仓储接口,而非实现. +* **领域服务(Domain Service)**: [领域服务](Domain-Services.md)是一种无状态的服务,它依赖多个聚合(实体)或外部服务来实现该领域的核心业务逻辑. +* **规约(Specification)**: [规约](Specifications.md)是一种**强命名**,**可重用**,**可组合**,**可测试**的实体过滤器. +* **领域事件(Domain Event)**: [领域事件](Event-Bus.md)是当领域某个事件发生时,通知其它领域服务的方式,为了解耦领域服务间的依赖. + +#### 应用层构建组成 + +* **应用服务(Application Service)**: [应用服务](Application-Services.md)是为实现用例的无状态服务.展现层调用应用服务获取DTO.应用服务调用多个领域服务实现用例.用例通常被视为一个工作单元. +* **数据传输对象(DTO)**: [DTO](Data-Transfer-Objects.md)是一个不含业务逻辑的简单对象,用于应用服务层与展现层间的数据传输. +* **工作单元(UOW)**: [工作单元](Unit-Of-Work.md)是事务的原子操作.UOW内所有操作,当成功时全部提交,失败时全部回滚. + +## 实现领域驱动:重点 + +### .NET解决方案分层 + +下图是使用[ABP的启动模板](Startup-Templates/Application.md)创建的解决方案: + +![domain-driven-design-vs-solution](images/domain-driven-design-vs-solution.png) + +解决方案名为 `IssueTracking` ,它包含多个项目.该解决方案出于**DDD原则**及**开发**和**部署**的实践来进行分层.后面会在各小节中介绍解决方案中的项目. + +> 在使用启动模板时,如果选择了其它类型的UI或*数据库提供程序*,解决方案的结构会略有不同.但是领域层和应用层是一样的,这才是DDD的重点.如果你想了解有关解决方案的更多信息,请参见[启动模板](Startup-Templates/Application.md). + +#### 领域层 + +领域层分为两个项目: + +* `IssueTracking.Domain`是**领域层中必需**的,它包含之前介绍的**构建组成**(实体,值对象,领域服务,规约,仓储接口等). +* `IssueTracking.Domain.Shared`是领域层中**很薄的项目**,它只包含领域层与其它层共享的数据类型的定义.例如,枚举,常量等. + +#### 应用层 + +应用层也被分为了两个项目: + +* `IssueTracking.Application.Contracts`包含**接口**的定义及接口依赖的**DTO**,此项目可以被展现层或其它客户端应用程序引用. +* `IssueTracking.Application`是**应用层中必需**的,它实现了`IssueTracking.Application.Contracts`项目中定义的接口. + +#### 展现层 + +* `IssueTracking.Web`是一个ASP.NET Core MVC / Razor Pages应用程序.它是提供UI元素及API服务的可执行程序. + +> ABP框架还支持其它类型的UI框架,包括[Angular](UI/Angular/Quick-Start.md)和[Blazor](UI/Blazor/Overall.md).当选择*Angular*或*Blazor*时,解决方案不会有`IssueTracking.Web`项目.但是会添加`IssueTracking.HttpApi.Host`在解决方案中,此项目会提供HTTP API供UI调用. + +#### 远程服务层 + +* `IssueTracking.HttpApi`包含了HTTP API的定义.它通常包含*MVC Controller* 和 *Model*(如果有).因此,你可以在此项目中提供HTTP API. + +> 大多数情况下,通过使用*API Controller* 包装应用服务,供客户端远程调用.ABP框架的[API自发现系统](API/Auto-API-Controllers.md)可以自动将应用服务公开为API,因此,通常不需要在此项目中再创建Controller.出于需要手动添加额外Controller的情况,也包含在模板解决方案中. + +* `IssueTracking.HttpApi.Client`当C#客户端应用程序需要调用`IssueTracking.HttpApi`的API时,这个项目非常有用.客户端程序仅需引用此项目就可以通过依赖注入方式,远程调用应用服务.它是通过ABP框架的[动态C#客户端API代理系统](API/Dynamic-CSharp-API-Clients.md)来实现的. + +> 在解决方案文件夹`test`下,有一个名为`IssueTracking.HttpApi.Client.ConsoleTestApp`的控制台程序.它演示了如何使用`IssueTracking.HttpApi.Client`项目来远程调用应用程序公开的API.因为只是演示,你可以删除此项目,再或者你认为`IssueTracking.HttpApi`不需要,同样可以删除. + +#### 基础设施层 + +你可能只创建一个基础设施项目来完成所有抽象类的定义及外部类的集成,又或者为不同的依赖创建多个不同的项目. + +我们建议采用一种平衡的方法:为主要的依赖的库(例如 Entity Framework Core)创建一个独立的项目,为其它的依赖库创建一个公共的基础设施项目. + +ABP的启动解决方案中包含两个用于集成Entity Framework Core的项目: + +* `IssueTracking.EntityFrameworkCore`是必需的,因为需要集成*EF Core*.应用程序的`数据库上下文(DbContext)`,数据库对象映射,仓储接口的实现,以及其它与*EF Core*相关的内容都位于此项目中. +* `IssueTracking.EntityFrameworkCore.DbMigrations`是管理Code First方式数据库迁移记录的特殊项目.此项目定义了一个独立的`DbContext`来追踪迁移记录.只有当添加一个新的数据库迁移记录或添加一个新的[应用模块](Modules/Index.md)时,才会使用此项目,否则,其它情况无需修改此项目内容. + +> 你可能想知道为什么会有两个EF Core项目,主要是因为[模块化](Module-Development-Basics.md).每个应用模块都有自己独立的`DbContext`,你的应用程序也有自己`DbContext`.`DbMigrations`项目包含**合并**所有模块迁移记录的**单个迁移路径**.虽然大多数情况下你无需过多了解,但也可以查看[EF Core迁移](Entity-Framework-Core-Migrations.md)了解更多信息. + +#### 其它项目 + +另外还有一个项目`IssueTracking.DbMigrator`,它是一个简单的控制台程序,用来执行数据库迁移,包括**初始化**数据库及创建**种子数据**.这是一个非常实用的应用程序,你可以在开发环境或生产环境中使用它. + +### 项目间的依赖关系 + +下图展示了解决方案中项目间的依赖关系(有些项目比较简单就未展示): + +![domain-driven-design-project-relations](images/domain-driven-design-project-relations.png) + +之前已介绍了这些项目.现在,我们来解释依赖的原因: + +* `Domain.Shared` 所有项目直接或间接依赖此项目.此项目中的所有类型都可以被其它项目所引用. +* `Domain` 仅依赖`Domain.Shared`项目,因为`Domain.Shared`本就属于领域层的一部分.例如,`Domain.Shared`项目中的枚举类型 `IssueType` 被`Domain`项目中的`Issue`实体所引用. +* `Application.Contracts` 依赖`Domain.Shared`项目,可以在DTO中重用`Domain.Shared`中的类型.例如,`Domain.Shared`项目中的枚举类型 `IssueType` 同样被`Contracts`项目中的`CreateIssueDto`DTO所引用. +* `Application` 依赖`Application.Contracts`项目,因为此项目需要实现应用服务的接口及接口使用的DTO.另外也依赖`Domain`项目,因为应用服务的实现必须依赖领域层中的对象. +* `EntityFrameworkCore` 依赖`Domain`项目,因为此项目需要将领域对象(实体或值对象)映射到数据库的表,另外还需要实现`Domain`项目中的仓储接口. +* `HttpApi` 依赖`Application.Contacts`项目,因为Controllers需要注入应用服务. +* `HttpApi.Client` 依赖`Application.Contacts`项目,因为此项目需要是使用应用服务. +* `Web` 依赖`HttpApi`项目,因为此项目对外提供HTTP APIs.另外Pages或Components 需要使用应用服务,所以还间接依赖了`Application.Contacts`项目 + +#### 虚线依赖 + +你在上图中会发现用虚线表示了另外两个依赖.`Web`项目依赖了 `Application` and `EntityFrameworkCore`,理论上`Web`不应该依赖这两个项目,但实际上依赖了.原因如下: + +`Web`是最终的运行程序,是负责托管Web的宿主,它在运行时需要**应用服务和仓储的实现类**. + +这种依赖关系的设计,可能会让你有机会在展现层直接使用到EF Core的对象,**应该严格禁止这样的做法**.如果想在解决方案分层上规避这种问题,有下面两种方式,相对复杂一些: + +* 将`Web`项目类型改为razor类库,并创建一个新项目,比如`Web.Host`,`Web.Host`依赖`Web`,`Application`,`EntityFrameworkCore`三个项目,并作为Web宿主程序运行.注意,不要写任何与UI相关的代码,只是作为**宿主运行**. +* 在`Web`项目中移除对`Application`和`EntityFrameworkCore`的引用,`Web`在启动时,再动态加载程序集``IssueTracking.Application.dll`和`IssueTracking.EntityFrameworkCore.dll`.可以使用ABP框架的[插件模块](PlugIn-Modules.md)来动态加载程序集. + +### DDD模式的应用程序执行顺序 + +下图展示了基于DDD模式下的Web应用程序执行顺序: + +![](images/domain-driven-design-web-request-flow.png) + +* 通常由UI(用例)发起一个HTTP请求到服务器. +* 由展现层(或分布式服务层)中的一个*MVC Controller*或*Razor Page Handler*处理请求,在这个阶段可以执行一些AOP逻辑([授权](Authorization.md),[验证](Validation.md),[异常处理](Exception-Handling.md)等),*MVC Controller*或*Razor Page Handler*调用注入的应用服务接口,并返回其调用后的结果(DTO).. +* 应用服务使用领域层的对象(实体,仓储接口,领域服务等)来实现UI(用例)交互.此阶段同样可以执行一些AOP逻辑(授权,验证等).应用服务中的每个方法应该是一个[工作单元](Unit-Of-Work.md),代表它是一次原子性操作. + +跨域问题大多数由**ABP框架自动实现**,通常不需要为此额外编码. + +### 通用原则 + +在详细介绍之前,我们先来看一些DDD的总体原则. + +#### 数据库提供程序 / ORM 独立原则 + +领域层和应用层应该与*数据库提供程序 / ORM*无关.领域层和应用层仅依赖仓储接口,并且仓储接口不依赖特定的ORM对象. + +原因如下: + +1. 未来领域层或应用层的基础设施会发生改变,例如,需要支持另外一种数据库类型,因此需要保持**领域层或应用层的基础设施是独立的**. +2. 将基础设施的实现隐藏在仓储中,使得领域层或应用层更**专注于业务逻辑代码**. +3. 可以通过模拟仓储接口,使得自动化测试更为方便. + +> 关于此原则, `EntityFrameworkCore`项目只被启动程序项目所引用,解决方案中其它项目均未引用. + +##### 关于数据库独立原则的讨论 + +**原因1**会非常影响你**领域对象的建模**(特别是实体间的关系)及**应用程序的代码**.假如,开始选择了关系型数据库,并使用了[Entity Framework Core](Entity-Framework-Core.md),后面尝试切换到[MongoDB](MongoDB.md),那么 **EF Core 中一些非常用的特性**你就不能使用了,例如: + +* 无法使用[变更追踪](https://docs.microsoft.com/zh-cn/ef/core/querying/tracking) ,因为*MongoDB provider*没有提供此功能,因此,你始终需要显式的更新已变更的实体. +* 无法在不同的聚合间使用[导航属性](https://docs.microsoft.com/zh-cn/ef/core/modeling/relationships),因为文档型数据库是不支持的.有关更多信息,请参见"规则:聚合间仅通过Id关联". + +如果你认为这些功能对你很**重要**,并且你永远都不会**离开** *EF Core*,那么我们认为你可以忽略这一原则.假如你在设计实体关系时使用了*EF Core*,你甚至可以在应用层引用*EF Core Nuget*包,并直接使用异步的LINQ扩展方法,例如 `ToListAsync()`(有关更多信息,请参见[仓储](Repositories.md)文档中的*IQueryable*和*Async Operations*). + +但是我们仍然建议采用仓储模式来隐藏基础设施中实现过程. + +#### 展现层技术无关原则 + +展现层技术(UI框架)时现代应用程序中最多变的部分之一.**领域层和应用层**应该对展现层所采用的技术或框架**一无所知**.使用ABP启动模板就非常容易实现此原则. + +在某些情况下,你可能需要在应用层和展现层中写重复的逻辑,例如,参数验证和授权检查.展现层检查出于**用户体验**,应用层或领域层检查出于**数据安全性**和**数据完整性**. + +#### 关注状态的变化,而不是报表/查询 + +DDD关注领域对象的**变化和相互作用**,如何创建或修改一个具有数据**完整性,有效性**,符合**业务规则**的实体对象. + +DDD忽略**领域对象的数据展示**,这并不意味着它们并不重要,如果应用程序没有精美的看板和报表,谁会愿意用呢?但是报表是另外一个讨论话题,你可以通过使用SQL Server报表功能或ElasticSearch来提供数据展示,又或者使用优化后的SQL查询语句,创建数据库索引或存储过程.唯一的原则是不要将这些内容混入领域的业务逻辑中. + +## 实现领域驱动:构建组成 + +这是本指南的重要部分,我们将通过示例介绍并解释一些**明确的规则**,在实现领域驱动设计时,你可以遵循这些规则并将其应用于解决方案中. + +### 领域 + +示例中会使用一些概念,这些概念在Github中被使用,例如, `Issue`(问题), `Repository`(仓库), `Label`(标签) 和`User`(用户).下图中展示了一些聚合,聚合根,实体,值对象以及它们之间的关系: + +![domain driven design example schema](images/domain-driven-design-example-domain-schema.png) + +**问题聚合** 由`Issue` 聚合根,及其包含的 `Comment` 和`IssueLabel` 集合组成.我们将重要讨论 `Issue` 聚合根: + +![domain-driven-design-issue-aggregate-diagram](images/domain-driven-design-issue-aggregate-diagram.png) + +### Aggregates + +如前所述, [聚合](Entities.md)是由**聚合根**包裹在一起的一组对象(实体和值对象).本节将介绍于聚合的有关原理和规则. + +> 后面的文档中,我们将使用 *实体* 替代 *聚合根* 或 *子集合实体* ,除非我们明确指明使用 *聚合根* 或 *子集合实体* . + +#### 聚合 / 聚合根 原则 + +##### 业务规则 + +实体负责实现与其自身属性相关的业务规则.同时*聚合根实体*还负责它们的子集合实体. + +聚合应该通过领域规则和约束来保证自身的**完整性**和**有效性**.这意味着,实体与DTO是不同的,实体应该比DTO多了些**实现业务逻辑的方法**.我们应该尽可能地在实体上实现业务规则. + +##### 独立单元 + +应该在一个独立的单元中完成**一个聚合的获取及保存**,包括自身属性及其子集合.假如我们需要在`Issue`中添加一个新的`Comment`,步骤如下: + +* 从数据库中获取一个 `Issue` 对象,包括所有子集合(`Comments`和`IssueLabels`). +* 使用 `Issue` 实体上的添加新`Comment`的方法,例如 `Issue.AddComment(...);`. +* 在数据库的单次操作中完成整个 `Issue`对象(包括子集合)的保存. + +对于在关系型数据库上用过 **EF Core** 的开发人员会认为,获取`Issue`的同时加载子集合没有必要并且还影响性能.为什么不使用SQL语句`Insert`来直接插入记录呢? + +这样做的原因是我们需要执行业务规则来保证数据的一致性和完整性.假如有一个业务规则:"用户不能对已锁定的问题进行评论".那如何在不查询数据库的情况下,获取问题是否已被锁定?所以,只有关联的对象都被加载了的时候,我们才可以执行业务规则. + +另外,使用**MongoDB**的开发人员就认为此原则很好理解.在MongoDB中,聚合对象(包含子集合)会被保存到一个`collection`中.因而,无需任何其它配置,就可以实现查询一个聚合,同时所有子对象. + +ABP框架有助于你实现这一原则 + +**例子: 问题追加评论** + +````csharp +public class IssueAppService : ApplicationService, IIssueAppService +{ + private readonly IRepository _issueRepository; + + public IssueAppService(IRepository issueRepository) + { + _issueRepository = issueRepository; + } + + [Authorize] + public async Task CreateCommentAsync(CreateCommentDto input) + { + var issue = await _issueRepository.GetAsync(input.IssueId); + issue.AddComment(CurrentUser.GetId(), input.Text); + await _issueRepository.UpdateAsync(issue); + } +} +```` + +通过`_issueRepository.GetAsync`方法来获取`Issue`对象时,默认就已经加载了所有子集合.对于MongoDB很简单,EF Core 则需要额外配置,一旦配置,ABP仓储类会自动处理.`_issueRepository.GetAsync`方法还有个可选参数`includeDetails`,可以传`false`,手动禁止加载子集合. + +> 如何配置及替代方案,请参考[EF Core document](Entity-Framework-Core.md)的*加载关联实体* 章节. + +`Issue.AddComment`接收两个参数,分别是`userId`和`text`,再执行自己的业务规则,最终将评论添加到`Issue`的评论集合中. + +最后,我们使用`_issueRepository.UpdateAsync`方法,将对象保存到数据库中. + +> EF Core 具有**变更追踪**的功能,因此,不需要调用`_issueRepository.UpdateAsync`方法.ABP的工作单元会再方法结束时,自动执行`DbContext.SaveChanges()`的.如果使用MongoDB则需要显式手动调用. +> +> 因此,当需要额外编写仓储层的实现,应该在实体变化时始终调用 `UpdateAsync` 方法. + +##### 事务边界 + +通常认为一个聚合就是一个事务边界.如果用例只涉及单个聚合,那么读取及修改就是一个操作单元.对聚合内所有对象的修改都将作为原子操作一起保存,无需显式创建数据库事务. + +但是,实际上,可能需要在一个用例中更改**多个聚合对象的实例**,并且还要求创建事务来保证**原子更新**和**数据一致性**.因此,ABP框架提供了为每个用例(应用服务中的方法),可以创建显式事务的功能.有关更多信息,请参见文档[工作单元](Unit-Of-Work.md). + +##### 序列化 + +一个聚合(包含聚合根及子集合)可以被序列化或反序列化.例如,MongoDB在保存对象到数据库时,会将聚合序列化为JSON文件,读取时再进行反序列化. + +> 使用关系型数据库+ORM时,这个原则不是必须的,但是,这是领域驱动设计的重要实践. + +以下规则遵循序列化原则 + +#### 聚合/聚合根规则及最佳实践 + +以下规则是遵循上述原则. + +##### 聚合间只通过ID相互引用 + +聚合应该只引用其它聚合的ID,也就是说,不允许定义导航属性关联至其它聚合. + +* 该规则遵循了可序列化原则. +* 该规则还可以避免不同聚合彼此间的相互操作以及业务逻辑的暴露. + +下图中,可以看到两个聚合根,`GitRepository` 和`Issue` : + +![domain-driven-design-reference-by-id-sample](images/domain-driven-design-reference-by-id-sample.png) + +* `GitRepository` 不应该包含 `Issue`的集合,因为`Issue`属于不同的聚合. +* `Issue` 不应该包含导航属性至 `GitRepository` .因为 `GitRepository`属于不同的聚合. +* `Issue` 可以有 `RepositoryId` 的引用. + +因此,若要获取`Issue`关联的 `GitRepository`对象,需要使用`Issue`的`RepositoryId`在数据库中进行一次查询. + +###### 对于EF Core和关系型数据库 + +MongoDB中不适合使用导航属性或集合的,原因是:当前源聚合对象会被序列化为JSON,其中会保存导航目标聚合的副本. + +在使用EF Core在关系型数据库上进行操作时,开发者可能认为此规则没必要.但是我们认为这是一条非常重要的规则,有助于降**低领域的复杂性**减少风险.我们强烈建议遵守此规则.如果你确定要忽略此规则,请参见上面的"关于数据库独立原理的讨论"章节. + +##### 保持聚合尽量的小 + +保持聚合简单而小巧是一个比较好的做法.因为聚合的读取与保存是一个整体,当处理较大对象时会出现性能问题,如下所示: + +![domain-driven-design-aggregate-keep-small](images/domain-driven-design-aggregate-keep-small.png) + +角色聚合包含`UserRole`值对象集合,方便查询该角色下有哪些用户.注意,`UserRole`不是聚合,并且也遵守了*聚合间只通过ID相互引用*的规则.但是在现实场景中,一个角色可能被给成千上万个用户,当从数据库中加载一个角色时,会关联加载数千个用户对象,这里会有 严重的性能问题. + +反过来看,`User`也可以有`Roles` 集合,现实中一个用户不会具有太多的角色,因此采用`User`关联`Roles`这种方式比较合适. + +当使用**非关系型数据库时**,`User`和`Role` 同时都有关联子集合,会出现另外一个问题.相同的记录会被重复记录在不同的集合中,并且难以保证数据一致性(需要添加记录到`User.Roles`和`Role.Users`中) + +因此,请根据以下注意事项来确定聚合的边界: + +* 同时被使用的对象. +* 查询(读取/保存)性能和内存消耗. +* 数据完整性,有效性,一致性. + +现实情况: + +* 大多数聚合根**没有子集合**. +* 子集合的数量控制在**100-150个**.如果集合数量超过150个,考虑将子对象改成聚合根. + +##### 聚合根 / 实体的主键 + +* 聚合根通常具有唯一的标识符ID (主键: PK).我们建议使用 `Guid`作为聚合根的主键类型. (原因请参见[Guid生成文档](Guid-Generation.md)). +* 聚合中的实体(非聚合根)可以使用联合主键. + +如图所示: + +![domain-driven-design-entity-primary-keys](images/domain-driven-design-entity-primary-keys.png) + +* `Organization`有一个`Guid`的标识符 (`Id`). +* `OrganizationUser`是一个子集合,使用 `OrganizationId` 和`UserId`作为联合主键. + +并不是所有的子集合的主键都是联合主键,有些情况下,可以使用单独的`Id`作为主键. + +> 联合主键实际上时关系型数据库中的概念,因为子集合对象有与之对应的数据库表,而表也要有主键.但是在非关系型数据库中,无需为子集合实体定义主键,因为它们本身就已属于一个聚合根. + +##### 聚合根 / 实体的构造函数 + +构造函数是实体生命周期的开始被执行.以下是构造函数的编写建议: + +* 将实体的**必填属性**作为构造函数参数,这样可以创建一个**有效(符合规则)的实体**.另外,将非必填属性作为构造函数的可选参数. +* 参数必须**检查有效性**. +* 所有**子集合**对象必须被初始化. + +**示例: `Issue` (聚合根) 构造函数** + +````csharp +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Volo.Abp; +using Volo.Abp.Domain.Entities; + +namespace IssueTracking.Issues +{ + public class Issue : AggregateRoot + { + public Guid RepositoryId { get; set; } + public string Title { get; set; } + public string Text { get; set; } + public Guid? AssignedUserId { get; set; } + public bool IsClosed { get; set; } + public IssueCloseReason? CloseReason { get; set; } //enum + + public ICollection Labels { get; set; } + + public Issue( + Guid id, + Guid repositoryId, + string title, + string text = null, + Guid? assignedUserId = null + ) : base(id) + { + RepositoryId = repositoryId; + Title = Check.NotNullOrWhiteSpace(title, nameof(title)); + + Text = text; + AssignedUserId = assignedUserId; + + Labels = new Collection(); + } + + private Issue() { /* for deserialization & ORMs */ } + } +} +```` + +* `Issue` 通过构造函数的参数,对必填属性进行赋值,而从创建一个有效的实体对象. +* 构造函数对参数进行**验证**(`Check.NotNullOrWhiteSpace(...)`必填项为空,抛出异常). +* 初始化子集合.在实例化`Issue`对象后,访问 `Labels` ,不会出现空指针异常. +* 构造函数还将 `id`传递给父类,不要在构造函数内生成 `Guid`(参阅 [Guid生成](Guid-Generation.md)). +* 为ORM保留**私有的无参构造函数**.防止编写代码时,意外使用了无参构造函数. + +> 参见 [实体](Entities.md) 文档,了解更多使用ABP框架创建实体的信息. + +##### 实体属性访问器和方法 + +上面的示例中,我们在构造函数中对 `Title` 进行了非空检查.但是开发人员可以再次对`Title`进行赋值. + +如果我们使用**公开的属性**,则无法在实体控制数据的**有效性**和**完整性**.建议: + +* 如果某个属性具有业务逻辑,则将该属性的**setter**改为私有. +* 定义公开的方法来修改属性. + +**示例:提供方法修改属性** + +````csharp +using System; +using Volo.Abp; +using Volo.Abp.Domain.Entities; + +namespace IssueTracking.Issues +{ + public class Issue : AggregateRoot + { + public Guid RepositoryId { get; private set; } //Never changes + public string Title { get; private set; } //Needs validation + public string Text { get; set; } //No validation + public Guid? AssignedUserId { get; set; } //No validation + public bool IsClosed { get; private set; } //Should change with CloseReason + public IssueCloseReason? CloseReason { get; private set; } //Should change with IsClosed + + //... + + public void SetTitle(string title) + { + Title = Check.NotNullOrWhiteSpace(title, nameof(title)); + } + + public void Close(IssueCloseReason reason) + { + IsClosed = true; + CloseReason = reason; + } + + public void ReOpen() + { + IsClosed = false; + CloseReason = null; + } + } +} +```` + +* `RepositoryId` setter是私有的.创建后无法变更,业务规则不允许将已有的问题移到其它仓库. +* `Title` setter是私有的.它的修改时需要加以验证. +* `Text` 和 `AssignedUserId` setter 是公开的.因为业务规则允许它们为空或任意值.我们认为没必要将它们改为私有的,如果将来业务发生变化,再将setter改为私有,并提供公开的方法进行修改.另外实体属于领域层,不会直接暴露属性给应用层(或其它层),目前将其公开不是什么大问题. +* `IsClosed` 和 `IssueCloseReason` 它们是一组属性.定义 `Close` 和 `ReOpen` 方法来同时对这两个属性进行赋值.这样可以防止问题被无故关闭. + +##### 业务逻辑与实体异常 + +对实体进行验证,或执行业务逻辑时,通常需要抛出异常: + +* 领域中定义的**特定的异常**. +* 实体方法中**抛出的异常**. + +**示例** + +````csharp +public class Issue : AggregateRoot +{ + //... + + public bool IsLocked { get; private set; } + public bool IsClosed { get; private set; } + public IssueCloseReason? CloseReason { get; private set; } + + public void Close(IssueCloseReason reason) + { + IsClosed = true; + CloseReason = reason; + } + + public void ReOpen() + { + if (IsLocked) + { + throw new IssueStateException( + "Can not open a locked issue! Unlock it first." + ); + } + + IsClosed = false; + CloseReason = null; + } + + public void Lock() + { + if (!IsClosed) + { + throw new IssueStateException( + "Can not open a locked issue! Unlock it first." + ); + } + + IsLocked = true; + } + + public void Unlock() + { + IsLocked = false; + } +} +```` + +这里有两个业务规则: + +* 已锁定的问题无法重新开启. +* 无法锁定已开打的问题. + +当违反业务规则时,`Issue` 会抛出 `IssueStateException` 异常: + +````csharp +using System; + +namespace IssueTracking.Issues +{ + public class IssueStateException : Exception + { + public IssueStateException(string message) + : base(message) + { + + } + } +} +```` + +抛出异常会引发两个问题: + +1. 当异常发生时,**用户**应该看到异常(错误)信息吗?如果需要看到,异常消息如何实现本地化? user** see the exception (error) message? If so, how do you **localize** the exception message? 实体中无法注入[本地化](Localization.md)的 `IStringLocalizer` 接口. +2. 对于Web应用或HTTP API,应向客户端返回什么**HTTP状态代码**. + +ABP框架的 [异常处理](Exception-Handling.md) 可以解决上述问题. + +**示例:使用异常编码** + +````csharp +using Volo.Abp; + +namespace IssueTracking.Issues +{ + public class IssueStateException : BusinessException + { + public IssueStateException(string code) + : base(code) + { + + } + } +} +```` + +* `IssueStateException` 继承至 `BusinessException` .对于`BusinessException`的派生类,ABP框架默认返回的HTTP状态码是403 (默认是服务器内部错误 状态码 500) +* 将`code` 作为Key,在本地化资源中查找对应的文字. + +现在,我们修 `ReOpen` 方法: + +````csharp +public void ReOpen() +{ + if (IsLocked) + { + throw new IssueStateException("IssueTracking:CanNotOpenLockedIssue"); + } + + IsClosed = false; + CloseReason = null; +} +```` + +> 使用常量而不是魔法字符串. + +在本地化资源文件中添加对应的记录: + +````json +"IssueTracking:CanNotOpenLockedIssue": "Can not open a locked issue! Unlock it first." +```` + +* 异常发生时,ABP将自动使用本地化消息(基于当前语言). +* 异常编码 (`IssueTracking:CanNotOpenLockedIssue` )会被发送到客户端.同样可以以编程方式处理此异常. + +> 你可以无需定义 `IssueStateException`,直接抛出`BusinessException`异常.详细信息,参见[异常处理文档](Exception-Handling.md) + +##### 实体中业务逻辑依赖外部服务时 + +仅依赖实体本身的属性执行的业务规非常简单.但是有时候,复杂的业务逻辑会**查询数据库**或使用[依赖注入](Dependency-Injection.md)中的其它服务,这该怎么办?注意:**实体是无法注入服务的**. + +实现这种业务逻辑有两种方式: + +* 将依赖的服务以**方法的参数**,传递到实体的业务逻辑方法中. +* 定义一个**领域服务**. + +领域服务我们后面再说.我们先看看在实体类中如何实现: + +**示例:业务规则: 不允许将3个以上未解决的问题关联到一个用户** + +````csharp +public class Issue : AggregateRoot +{ + //... + public Guid? AssignedUserId { get; private set; } + + public async Task AssignToAsync(AppUser user, IUserIssueService userIssueService) + { + var openIssueCount = await userIssueService.GetOpenIssueCountAsync(user.Id); + + if (openIssueCount >= 3) + { + throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit"); + } + + AssignedUserId = user.Id; + } + + public void CleanAssignment() + { + AssignedUserId = null; + } +} +```` + +* `AssignedUserId` 私有的属性setter.此属性只能通过`AssignToAsync` 和`CleanAssignment` 方法来修改. +* `AssignToAsync` 通过 `user.Id` 属性获取一个 `AppUser` 实体. +* `IUserIssueService` 是获取用户未解决问题的服务. +* `AssignToAsync` 当不满足业务规则时抛出异常. +* 最后,符合规则,就对属性`AssignedUserId` 进行赋值. + +这样就解决了将问题关联到用户时,需要调用外部服务的问题,但是它也存在几个问题: + +* 实体**依赖了外部服务**,实体变得**复杂**. +* 实体调用变的复杂.在调用`AssignToAsync` 方法时还需要传递 `IUserIssueService` 服务. + +实现这种业务逻辑另外一种方案,使用领域服务,后面将详细说明. + +### 仓储 + +[仓储](Repositories.md)是一个类集合的接口,它通常被领域层或应用层调用,负责访问持久化系统(数据库),读取写入业务对象(聚合). + +仓储的原则: + +* 在**领域层**中定义仓储接口,因为仓储会被领域层或应用层调用,在**基础设施层中实现**(*EntityFrameworkCore* 项目). +* 仓储中**不要写任何业务逻辑**. +* 仓储接口不依赖 **数据库提供程序 / ORM**.例如,不要在仓储中返回 `DbSet` 类型,因为 `DbSet`是EF Core中的对象. +* **仅为聚合根定义仓储**,非聚合根对象不要提供仓储,因为子集合可以通过聚合根来进行持久化. + +#### 仓储中不要写任何业务逻辑 + +我们经常不小心把业务逻辑编写到了仓储层. + +**示例:从仓储中获取非活动的问题** + +````csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace IssueTracking.Issues +{ + public interface IIssueRepository : IRepository + { + Task> GetInActiveIssuesAsync(); + } +} +```` + +`IIssueRepository`继承至`IRepository<...>`接口,并新增一个新接口`GetInActiveIssuesAsync`.此仓储为聚合根`Issue`提供查询的实现. + +````csharp +public class Issue : AggregateRoot, IHasCreationTime +{ + public bool IsClosed { get; private set; } + public Guid? AssignedUserId { get; private set; } + public DateTime CreationTime { get; private set; } + public DateTime? LastCommentTime { get; private set; } + //... +} +```` + +(上面的属性仅为了演示此示例) + +原则要求仓储不包含业务逻辑,上面的示例"**什么是非活动的问题?**"这个属于业务规则吗? + +````csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using IssueTracking.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace IssueTracking.Issues +{ + public class EfCoreIssueRepository : + EfCoreRepository, + IIssueRepository + { + public EfCoreIssueRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task> GetInActiveIssuesAsync() + { + var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30)); + + return await DbSet.Where(i => + + //Open + !i.IsClosed && + + //Assigned to nobody + i.AssignedUserId == null && + + //Created 30+ days ago + i.CreationTime < daysAgo30 && + + //No comment or the last comment was 30+ days ago + (i.LastCommentTime == null || i.LastCommentTime < daysAgo30) + + ).ToListAsync(); + } + } +} +```` + +(使用EF Core来实现. 如何使用EF Core实现仓储,请参见[EF Core集成文档](Entity-Framework-Core.md) ) + +来看一下`GetInActiveIssuesAsync`的实现,可以看到定义了一个**非活动问题的业务规则**: + +- 是*open*的(非*IsClosed* ) +- 没有关联到任何人 +- 创建时间大于30天 +- 最近30天没有评论 + +这个业务逻辑就被实现再了仓储内部,当我们需要重用这个业务规则时就会出现问题. + +例如:我们需要再实体`Issue`上添加一个方法来判断是否非活动`bool IsInActive()`,以方便我们在`Issue`实例上获取. + +代码如下: + +````csharp +public class Issue : AggregateRoot, IHasCreationTime +{ + public bool IsClosed { get; private set; } + public Guid? AssignedUserId { get; private set; } + public DateTime CreationTime { get; private set; } + public DateTime? LastCommentTime { get; private set; } + //... + + public bool IsInActive() + { + var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30)); + return + //Open + !IsClosed && + + //Assigned to nobody + AssignedUserId == null && + + //Created 30+ days ago + CreationTime < daysAgo30 && + + //No comment or the last comment was 30+ days ago + (LastCommentTime == null || LastCommentTime < daysAgo30); + } +} +```` + +我们需要拷贝代码来实现,如果将来业务规则发送变化,我们就必须修改这两处的代码,这样做非常危险. + +这里有一个很好的解决方案,就是使用*规约模式*. + +### 规约模式 + +[规约](Specifications.md)是一种**强命名**,**可重用**,**可组合**,**可测试**的实体过滤器. + +ABP框架提供了基础设施来轻松定义规约类,你可以在代码中方便使用.我们来将非活动问题使用规约方式实现: + +````csharp +using System; +using System.Linq.Expressions; +using Volo.Abp.Specifications; + +namespace IssueTracking.Issues +{ + public class InActiveIssueSpecification : Specification + { + public override Expression> ToExpression() + { + var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30)); + return i => + + //Open + !i.IsClosed && + + //Assigned to nobody + i.AssignedUserId == null && + + //Created 30+ days ago + i.CreationTime < daysAgo30 && + + //No comment or the last comment was 30+ days ago + (i.LastCommentTime == null || i.LastCommentTime < daysAgo30); + } + } +} +```` + +基类`Specification`通过表达式简化了创建规约的过程,仅需要将仓储中的表达式迁移至规约中. + +现在,我们可以在`Issue` 和 `EfCoreIssueRepository`中重用规约`InActiveIssueSpecification`了. + +#### 在实体内使用规约 + +`Specification` 类提供了一个`IsSatisfiedBy`方法,在实例对象上应用规约检查,判断是否满足规约的要求.代码如下: + +````csharp +public class Issue : AggregateRoot, IHasCreationTime +{ + public bool IsClosed { get; private set; } + public Guid? AssignedUserId { get; private set; } + public DateTime CreationTime { get; private set; } + public DateTime? LastCommentTime { get; private set; } + //... + + public bool IsInActive() + { + return new InActiveIssueSpecification().IsSatisfiedBy(this); + } +} +```` + +实例化一个新的规约`InActiveIssueSpecification`实例,并通过`IsSatisfiedBy` 方法进行规约检查. + +#### 在仓储内使用规约 + +首先,我们先修改一下仓储接口: + +````csharp +public interface IIssueRepository : IRepository +{ + Task> GetIssuesAsync(ISpecification spec); +} +```` + +先将`GetInActiveIssuesAsync` 方法改名为`GetIssuesAsync` ,因为我们改为使用规约方式,现在就无需为不同的查询条件创建不同的接口方法(例如:`GetAssignedIssues(...)`,`GetLockedIssues(...)`) + +再修改下仓储实现: + +````csharp +public class EfCoreIssueRepository : + EfCoreRepository, + IIssueRepository +{ + public EfCoreIssueRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task> GetIssuesAsync(ISpecification spec) + { + return await DbSet + .Where(spec.ToExpression()) + .ToListAsync(); + } +} +```` + +由于`ToExpression()`方法返回一个表达式,因此可以直接将其传递给`Where`方法来过滤实体. + +我们可以在调用`GetIssuesAsync`方法时,传递任何规约的实例. + +````csharp +public class IssueAppService : ApplicationService, IIssueAppService +{ + private readonly IIssueRepository _issueRepository; + + public IssueAppService(IIssueRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task DoItAsync() + { + var issues = await _issueRepository.GetIssuesAsync( + new InActiveIssueSpecification() + ); + } +} +```` + +##### 默认仓储使用规约 + +实际上,我们不必非要创建一个自定义仓储来使用规约方式,泛型仓储`IRepository`同样可以使用规约,因为`IRepository`已扩展了`IQueryable`对象,因此可以在泛型仓储上使用,代码如下: + +````csharp +public class IssueAppService : ApplicationService, IIssueAppService +{ + private readonly IRepository _issueRepository; + + public IssueAppService(IRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task DoItAsync() + { + var issues = AsyncExecuter.ToListAsync( + _issueRepository.Where(new InActiveIssueSpecification()) + ); + } +} +```` + +`AsyncExecuter`是ABP框架提供的一个异步LINQ扩展方法(与`ToListAsync`类似),这个方法不依赖依赖EF Core,请参见[仓储文档](Repositories.md). + +#### 组合规约 + +规约强大的能力就是可组合.假设我们还有一个业务规则:`Issue` 仅在里程碑中才返回`true`: + +````csharp +public class MilestoneSpecification : Specification +{ + public Guid MilestoneId { get; } + + public MilestoneSpecification(Guid milestoneId) + { + MilestoneId = milestoneId; + } + + public override Expression> ToExpression() + { + return i => i.MilestoneId == MilestoneId; + } +} +```` + +此规约与`InActiveIssueSpecification`的区别,它是有参数的.我们可以组合两种规约来实现获取指定里程碑下的非活动问题列表 + +````csharp +public class IssueAppService : ApplicationService, IIssueAppService +{ + private readonly IRepository _issueRepository; + + public IssueAppService(IRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task DoItAsync(Guid milestoneId) + { + var issues = AsyncExecuter.ToListAsync( + _issueRepository + .Where( + new InActiveIssueSpecification() + .And(new MilestoneSpecification(milestoneId)) + .ToExpression() + ) + ); + } +} +```` + +上面的示例使用了`And`扩展方法来组合规约.还有更多的组合方法,如:`Or(...)`和`AndNot(...)`. + +> 有关ABP框架提供的规约更多信息,请参见[规约文档](Specifications.md). + +### 领域服务 + +领域服务主要来实现本领域的逻辑: + +* 依赖**服务和仓储**. +* 需要使用多个聚合. + +领域服务和领域对象一起使用.领域服务可以获取并返回**实体**,**值对象**等,它们不返回**DTO**.DTO属于应用层的一部分. + +**示例:用户关联一个问题** + +需要在`Issue`实体中实现问题的关联: + +````csharp +public class Issue : AggregateRoot +{ + //... + public Guid? AssignedUserId { get; private set; } + + public async Task AssignToAsync(AppUser user, IUserIssueService userIssueService) + { + var openIssueCount = await userIssueService.GetOpenIssueCountAsync(user.Id); + + if (openIssueCount >= 3) + { + throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit"); + } + + AssignedUserId = user.Id; + } + + public void CleanAssignment() + { + AssignedUserId = null; + } +} +```` + +现在我们把逻辑迁移到领域服务中实现. + +首先,修改一下 `Issue` 类: + +````csharp +public class Issue : AggregateRoot +{ + //... + public Guid? AssignedUserId { get; internal set; } +} +```` + +* 删除关联的相关方法. +* 修改属性 `AssignedUserId` 的 setter 为 `internal`,以允许领域服务可以修改. + +下一步是创建一个名为`IssueManager`的领域服务,此领域服务的`AssignToAsync`方法负责将问题关联至指定的用户. + +````csharp +public class IssueManager : DomainService +{ + private readonly IRepository _issueRepository; + + public IssueManager(IRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task AssignToAsync(Issue issue, AppUser user) + { + var openIssueCount = await _issueRepository.CountAsync( + i => i.AssignedUserId == user.Id && !i.IsClosed + ); + + if (openIssueCount >= 3) + { + throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit"); + } + + issue.AssignedUserId = user.Id; + } +} +```` + +`IssueManager`可以注入其它服务,来查询指定用户已经关联的未解决问题数量. + +> 我们建议使用 `Manager` 后缀来命名领域服务. + +这种设计的唯一缺陷是可以在类`Issue`外部修改`Issue.AssignedUserId`属性.但是它的访问级别是`internal`而非`public`,在`IssueTracking.Domain`项目内部才能被修改,我们认为这样是合理的: + +* 开发人员清楚领域层的开发规则,他们会使用`IssueManager`来执行业务逻辑. +* 应用层开发人员只能使用`IssueManager`,因此他们无法直接修改实体属性. + +尽管两种方式有各自的优势,但我们更喜欢创建领域服务并注入其它服务来执行业务逻辑这种方式. + +### 应用服务 + +应用服务是实现**用例**的无状态服务.应用服务通常**获取并返回DTO**.应用服务被展现层所使用,应用服务**调用领域对象**(实体,仓储等)来实现用例. + +应用服务的通用原则: + +* 实现特定用例的**应用程序逻辑**,不要在应用服务内实现核心领域的逻辑. +* 应用服务的方法**不要返回实体**.始终只返回DTO. + +**示例:用户关联一个问题** + +````csharp +using System; +using System.Threading.Tasks; +using IssueTracking.Users; +using Microsoft.AspNetCore.Authorization; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace IssueTracking.Issues +{ + public class IssueAppService : ApplicationService, IIssueAppService + { + private readonly IssueManager _issueManager; + private readonly IRepository _issueRepository; + private readonly IRepository _userRepository; + + public IssueAppService( + IssueManager issueManager, + IRepository issueRepository, + IRepository userRepository) + { + _issueManager = issueManager; + _issueRepository = issueRepository; + _userRepository = userRepository; + } + + [Authorize] + public async Task AssignAsync(IssueAssignDto input) + { + var issue = await _issueRepository.GetAsync(input.IssueId); + var user = await _userRepository.GetAsync(input.UserId); + + await _issueManager.AssignToAsync(issue, user); + + await _issueRepository.UpdateAsync(issue); + } + } +} +```` + +应用服务的方法通常包含三个步骤: + +1. 从数据库获取用例所需的领域对象. +2. 使用领域对象(领域服务,实体等)执行业务逻辑. +3. 将实体的变更持久化至数据库. + +> 如果使用的是EF Core,第三步不是必须的,因为EF Core有追踪实体变化的功能.如果要利用此功能,请参阅上面的"关于数据库独立原则的讨论"章节. + +`IssueAssignDto` 是本示例中一个简单的DTO对象: + +````csharp +using System; + +namespace IssueTracking.Issues +{ + public class IssueAssignDto + { + public Guid IssueId { get; set; } + public Guid UserId { get; set; } + } +} +```` + +### 数据传输对象 + +[DTO](Data-Transfer-Objects.md)是应用层与展现层间传输数据的简单对象.应用服务方法获取并返回Dto. + +#### DTO通用原则和最佳实践 + +* DTO应该是**可被序列化**的.因为大所数情况下,DTO是通过网络传输的,因此它应该具有**无参的构造函数**. +* 不应该包含任何**业务逻辑**. +* **切勿**继承或引用**实体**. + +**输入DTO**(应用服务方法的参数)与 **输出DTO** (应用服务方法的返回对象)具有不同的作用,因此,它们应该区别对待. + +#### 输入DTO 最佳实践 + +##### 不要在输入DTO中定义不使用的属性 + +**仅**在输入DTO中定义用例**所需要的属性**!否则,会造成调用应用服务的客户端产生困惑. + +这个规则好像没什么必要,因为没人会在方法参数(输入DTO)中添加无用的属性.但是,有时候,特别是在重用DTO时,输入DTO会包含无用的属性. + +##### 不要重用输入DTO + +**为每个用例**(应用服务的方法)单独定义一个**专属的输入DTO**.否则,在一些情况下,会添加一些不被使用的属性,这样就违反上面的规则:不要在输入DTO中定义不使用的属性. + +在两个用例中重用相同的DTO似乎很有吸引力,因为它们的属性是一模一样的.现阶段它们是一样的,但是随着业务变化,可能它们会产生差异,届时你可能还是需要进行拆分.**和用例间的耦合相比,代码的复制可能是更好的做法**. + +重用输入DTO的另外一种方式是**继承**DTO,这同样会产生上面描述的问题. + +**示例:用户应用服务** + +````csharp +public interface IUserAppService : IApplicationService +{ + Task CreateAsync(UserDto input); + Task UpdateAsync(UserDto input); + Task ChangePasswordAsync(UserDto input); +} +```` + +`UserDto`作为`IUserAppService`所有方法的输入DTO,代码如下: + +````csharp +public class UserDto +{ + public Guid Id { get; set; } + public string UserName { get; set; } + public string Email { get; set; } + public string Password { get; set; } + public DateTime CreationTime { get; set; } +} +```` + +对于上面的示例: + +* `Id` 属性在 *Create* 方法中,没有被使用,因为`Id`由服务器生成. +* `Password` 属性在 *Update* 方法中,没有被使用.因为有修改密码的单独方法. +* `CreationTime` 属性未被使用,因为不允许客户端发送创建时间属性,这个应该由服务器生成. + +较好的做法应该这样: + +````csharp +public interface IUserAppService : IApplicationService +{ + Task CreateAsync(UserCreationDto input); + Task UpdateAsync(UserUpdateDto input); + Task ChangePasswordAsync(UserChangePasswordDto input); +} +```` + +下面是输入DTO的定义: + +````csharp +public class UserCreationDto +{ + public string UserName { get; set; } + public string Email { get; set; } + public string Password { get; set; } +} + +public class UserUpdateDto +{ + public Guid Id { get; set; } + public string UserName { get; set; } + public string Email { get; set; } +} + +public class UserChangePasswordDto +{ + public Guid Id { get; set; } + public string Password { get; set; } +} +```` + +虽然编写了更多的代码,但是这样可维护性更高. + +**例外情况:**该规则有一些例外的情况,例如,你想开发两个方法,它们共用相同的输入DTO(通过继承或重用),有一个报表页面有多个过滤条件,多个应用服务使用相同的输入参数返回不同的结果(如,大屏展示数据,Excel报表,csv报表).这种情况下,你是需要修改一个参数,多个应用服务都应该一起被修改. + +##### 输入DTO中验证逻辑 + +- 仅在DTO内执行**简单验证**.使用数据注解验证属性或通过`IValidatableObject` 方式. +- **不要执行领域验证**.例如,不要在DTO中检查用户名是否唯一的验证. + +**示例:使用注解方式** + +````csharp +using System.ComponentModel.DataAnnotations; + +namespace IssueTracking.Users +{ + public class UserCreationDto + { + [Required] + [StringLength(UserConsts.MaxUserNameLength)] + public string UserName { get; set; } + + [Required] + [EmailAddress] + [StringLength(UserConsts.MaxEmailLength)] + public string Email { get; set; } + + [Required] + [StringLength(UserConsts.MaxEmailLength, + MinimumLength = UserConsts.MinPasswordLength)] + public string Password { get; set; } + } +} +```` + +当输入无效时,ABP框架会自动验证输入DTO,抛出`AbpValidationException`异常,并向客户返回`400`的HTTP状态码. + +> 一些开发人员认为最好将验证规则和DTO分离.我们认为声明性(数据注解)方式是比较实用的,不会引起任何设计问题.如果你喜欢其它方式,ABP还支持[FluentValidation继承](FluentValidation.md).有关所有验证的详细文档,请参见[验证文档](Validation.md). + +#### 输出DTO最佳实践 + +* 保持**数量较少**的输出DTO,尽可能**重用输入DTO**(例外:不要将输入DTO作为输出DTO). +* 输出DTO可以包含比用例需要的属性**更多**的属性. +* 针对 **Create** 和 **Update** 方法,返回实体的DTO. + +以上建议的原因是: + +* 使客户端代码易于开发和扩展: + * 客户端处理**相似但不相同**的DTO是有问题的. + * 将来UI或客户端通常会使用到DTO上的**其它属性**.返回实体的所有属性,可以在无需修改服务端代码的情况下,只修改客户端代码. + * 在开放API给**第三方客户端**时,避免不同需求的返回不同的DTO. +* 使服务器端代码易于开发和扩展: + * 你需要**维护**的类的数量较少. + * 你可以重用Entity->DTO**对象映射**的代码. + * 不同的方法返回相同的类型,可以使得在**添加新方法**时变的简单明了. + +**示例:不同的方法返回不同的DTO** + +````csharp +public interface IUserAppService : IApplicationService +{ + UserDto Get(Guid id); + List GetUserNameAndEmail(Guid id); + List GetRoles(Guid id); + List GetList(); + UserCreateResultDto Create(UserCreationDto input); + UserUpdateResultDto Update(UserUpdateDto input); +} +```` + +> 这里我们没有使用异步方式,是为了示例更清晰,你实际代码中应该使用异步方式) + +上面的示例代码中,每个方法都返回了不同的DTO类型,这样处理,会导致查询数据,映射对象都会有很多重复的代码. + +应用服务`IUserAppService` 可以简化成如下代码: + +````csharp +public interface IUserAppService : IApplicationService +{ + UserDto Get(Guid id); + List GetList(); + UserDto Create(UserCreationDto input); + UserDto Update(UserUpdateDto input); +} +```` + +只需使用一个DTO对象 + +````csharp +public class UserDto +{ + public Guid Id { get; set; } + public string UserName { get; set; } + public string Email { get; set; } + public DateTime CreationTime { get; set; } + public List Roles { get; set; } +} +```` + +* 删除 `GetUserNameAndEmail`和`GetRoles` 方法,因为,返回的DTO中已经包含了对应的信息. +* `GetList`方法的返回的泛型类型与`Get`方法的返回类型一致. +* `Create` 与 `Update`的返回类型都是 `UserDto`. + +如上所述,使用相同的DTO有很多优点.例如,我们在UI上使用**表格**展现用户集合,再用户数据更新后,我们可以获取到返回对象,并对**表格数据源进行更新**.因此,我们无需再次调用`GetList`来获取全部数据.这就是我们为什么建议`Create` 与 `Update`方法都返回相同`UserDto`的原因. + +##### 讨论 + +输出DTO的建议并不适用于所有情况.出于**性能**原因,我们可以忽略这些建议,尤其是在返回**大量数据**,为UI定制,**并发量较高**时. + +在这些情况下,你可以定制仅包含**必要信息的DTO**.上面的建议只适用于额外多些属性并**不会损失太多性能**,并关注代码**可维护**的应用系统. + +#### 对象映射到对象 + +当两个对象具有相同或相似的属性,自动将[对象映射到对象](Object-To-Object-Mapping.md)是一种将值从一个对象复制到另外一个对象非常有用的方法. + +DTO和实体通常具有相同或相似的属性,你经常需要从一个实体创建一个DTO对象.相较于手动映射,基于[AutoMapper](http://automapper.org/)的ABP[对象映射系统](Object-To-Object-Mapping.md),更加方便简单. + +* **仅**在**实体=>输出DTO**的时候使用自动映射. +* 不要在**输入DTO=>Entity**的时候使用自动映射. + +因为以下原因,你不应该在输入DTO=>Entity的时候使用自动映射: + +1. 实体类通常具有一个**构造函数**,该构造函数带有参数,确保创建有效的对象,而自动对象映射通常需要一个无参构造函数. +2. 大多数实体中的属性setter是**私有的**,你只能调用实体上的方法来修改属性. +3. 另外,需要进行对用户或客户端的**输入参数进行验证**,而不是盲目映射到实体属性上. + +尽管其中一些问题可以额外配置映射来解决(如,AutoMapper允许自定义映射规则),但这样会使业务逻辑被**耦合到基础设施代码中**.我们认为业务代码应该明确,清晰且易于理解. + +有关此部分的建议,请参加下面的"*实体创建*"部分 + +## 用例 + +本节将演示一些用例,并讨论替代方案 + +### 实体创建 + +实体或聚合根的创建,是实体生命周期的开始."*聚合/聚合根规则及最佳实践*"章节中建议为Entity类定义**一个主构造函数**,以确保创建一个**有效的实体**.因此,需要创建该实体对象实例时,都应该**使用该构造函数**. + + `Issue` 聚合根的代码如下: + +````csharp +public class Issue : AggregateRoot +{ + public Guid RepositoryId { get; private set; } + public string Title { get; private set; } + public string Text { get; set; } + public Guid? AssignedUserId { get; internal set; } + + public Issue( + Guid id, + Guid repositoryId, + string title, + string text = null + ) : base(id) + { + RepositoryId = repositoryId; + Title = Check.NotNullOrWhiteSpace(title, nameof(title)); + Text = text; //Allow empty/null + } + + private Issue() { /* Empty constructor is for ORMs */ } + + public void SetTitle(string title) + { + Title = Check.NotNullOrWhiteSpace(title, nameof(title)); + } + + //... +} +```` + +* 通过其非空参数的构造函数创建有效的实体. +* 如需修改 `Title` 属性,必须通过`SetTitle` 方法,来确保被设置值的有效性. +* 如需将此问题关联至用户,则需要使用`IssueManager`(关联前需要执行一些业务逻辑,相关逻辑参见上面的"*领域服务*"部分) +* `Text` 属性setter是公开的,因为它可以为null,并且本示例中也没有验证规则,它在构造函数中也是可选的. + +创建问题的应用服务代码: + +````csharp +public class IssueAppService : ApplicationService, IIssueAppService +{ + private readonly IssueManager _issueManager; + private readonly IRepository _issueRepository; + private readonly IRepository _userRepository; + + public IssueAppService( + IssueManager issueManager, + IRepository issueRepository, + IRepository userRepository) + { + _issueManager = issueManager; + _issueRepository = issueRepository; + _userRepository = userRepository; + } + + public async Task CreateAsync(IssueCreationDto input) + { + // Create a valid entity + var issue = new Issue( + GuidGenerator.Create(), + input.RepositoryId, + input.Title, + input.Text + ); + + // Apply additional domain actions + if (input.AssignedUserId.HasValue) + { + var user = await _userRepository.GetAsync(input.AssignedUserId.Value); + await _issueManager.AssignToAsync(issue, user); + } + + // Save + await _issueRepository.InsertAsync(issue); + + // Return a DTO represents the new Issue + return ObjectMapper.Map(issue); + } +} +```` + +`CreateAsync` 方法; + +* 使用 `Issue` **构造函数** 创建一个有效的问题.`Id` 属性通过[IGuidGenerator](Guid-Generation.md)服务生成.此处没有使用对象自动映射. +* 如果需要将**问题关联至用户**,则通过 `IssueManager`来执行关联逻辑. +* **保存** 实体至数据库. +* 最后,使用 `IObjectMapper` 将`Issue`实体**映射**为 `IssueDto` 并返回. + +#### 在创建实体时执行领域规则 + +`Issue`除了在构造函数中进行了一些简单验证外,示例中没其它业务验证.在有些情况下,在创建实体时会有一些其它业务规则. + +假如,已经存在一个完全相同的问题,那么就不要再创建问题.这个规则应该在哪里执行?在**应用服务中执行是不对的**,因为它是**核心业务(领域)的规则**,应该将此规则在领域服务中执行.在这种情况下,我们应该在`IssueManager`中执行此规则,因此应该强制应用服务调用领域服务`IssueManager`来新建`Issue`. + +首先修改 `Issue` 构造函数的访问级别为 `internal`: + +````csharp +public class Issue : AggregateRoot +{ + //... + + internal Issue( + Guid id, + Guid repositoryId, + string title, + string text = null + ) : base(id) + { + RepositoryId = repositoryId; + Title = Check.NotNullOrWhiteSpace(title, nameof(title)); + Text = text; //Allow empty/null + } + + //... +} +```` + +这样可以防止,应用服务直接使用`Issue` 的构造函数去创建`Issue` 实例,必须使用 `IssueManager`来创建.然后我们再添加一个`CreateAsync`方法: + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; + +namespace IssueTracking.Issues +{ + public class IssueManager : DomainService + { + private readonly IRepository _issueRepository; + + public IssueManager(IRepository issueRepository) + { + _issueRepository = issueRepository; + } + + public async Task CreateAsync( + Guid repositoryId, + string title, + string text = null) + { + if (await _issueRepository.AnyAsync(i => i.Title == title)) + { + throw new BusinessException("IssueTracking:IssueWithSameTitleExists"); + } + + return new Issue( + GuidGenerator.Create(), + repositoryId, + title, + text + ); + } + } +} +```` + +* `CreateAsync` 方法会检查标题是否已经存在,当有相同标题的问题时,会抛出业务异常. +* 如果标题没有重复的,则创建并返回一个新的 `Issue`对象. + +再修改`IssueAppService` 的代码,来调用 `IssueManager`的 `CreateAsync` 方法: + +````csharp +public class IssueAppService : ApplicationService, IIssueAppService +{ + private readonly IssueManager _issueManager; + private readonly IRepository _issueRepository; + private readonly IRepository _userRepository; + + public IssueAppService( + IssueManager issueManager, + IRepository issueRepository, + IRepository userRepository) + { + _issueManager = issueManager; + _issueRepository = issueRepository; + _userRepository = userRepository; + } + + public async Task CreateAsync(IssueCreationDto input) + { + // Create a valid entity using the IssueManager + var issue = await _issueManager.CreateAsync( + input.RepositoryId, + input.Title, + input.Text + ); + + // Apply additional domain actions + if (input.AssignedUserId.HasValue) + { + var user = await _userRepository.GetAsync(input.AssignedUserId.Value); + await _issueManager.AssignToAsync(issue, user); + } + + // Save + await _issueRepository.InsertAsync(issue); + + // Return a DTO represents the new Issue + return ObjectMapper.Map(issue); + } +} + +// *** IssueCreationDto class *** +public class IssueCreationDto +{ + public Guid RepositoryId { get; set; } + [Required] + public string Title { get; set; } + public Guid? AssignedUserId { get; set; } + public string Text { get; set; } +} +```` + +##### 讨论:为什么`IssueManager`中没有执行`Issue`的保存? + +你可能会问"**为什么`IssueManager`中没有执行`Issue`的保存?**".我们认为这是应用服务的职责. + +因为,应用服务可能在保存`Issue`对象之前,需要对其它对象进行修改.如果领域服务执行了保存,那么*保存*操作就是重复的. + +* 会触发两次数据库会交互,这会导致性能损失. +* 需要额外添加显式的事务来包含这两个操作,才能保证数据一致性. +* 如果因为业务规则取消了实体的创建,则应该在数据库事务中回滚事务,取消所有操作. + +假如在`IssueManager.CreateAsync`中先保存一次数据,那么数据会先执行一次*Insert*操作,后面关联用户的逻辑执行后,又会再执行一次*Update*操作. + +如果不在`IssueManager.CreateAsync`中保存数据,那么,新建`Issue`和关联用户,只会执行一次*Insert*操作. + +##### 讨论:为什么没有在应用服务中执行标题是否重复的检查? + +简单地说"因为它是**核心领域逻辑**,应该在领域层实现".这又带来一个新问题,"**如何确定**是领域层逻辑,还是应用层逻辑"?(这个我们后面再详细讨论) + +对于此示例,可以用一个简单的问题来判断到底是领域逻辑还是应用逻辑:"如果还有另外一种创建`Issue`的方式(用例),我们是否还需要执行?如果需要执行,就属于领域层逻辑,不需要执行就是应用层逻辑".你可能认为为什么还有别的用例来创建`Issue`呢? + +* 应用程序的**最终用户**可能会在UI上创建`Issue`. +* 系统内部人员,可以在**后台管理**端采用另外一种方式创建`Issue`(这种情况下,可能使用不同的业务规则). +* 对**第三方客户端**开放的API,它们的规则又有所不同. +* 还有**后台作业系统**会执行某些操作时创建`Issue`,这样,它是在没有任何用户交互情况下创建`Issue`. +* 还有可能是UI上某个按钮,可以将某些内容(例如,讨论)转为`Issue`. + +我们还可以举更多例子.所有这些都应该通过**不同的应用服务方法来实现**(请参见下面的"*多个应用服务层*"部分),但是它们**始终遵循**以下的规则: + +新的问题标题不能与任何已有的问题标题相同.这就是为什么说的"*标题是否重复的检查*"属于核心领域逻辑的原因,这个逻辑应该在领域层,而**不应该**在应用层的所有方法中**去重复**定义. + +### 修改实体 + +创建实体后,将根据用例对实体进行修改,直到将其从系统中删除.可以有不同的用例直接或间接的修改实体. + +在本节中,我们将讨论一种典型的修改操作,该操作会修改`Issue`的多个属性. + +从*Update* DTO开始: + +````csharp +public class UpdateIssueDto +{ + [Required] + public string Title { get; set; } + public string Text { get; set; } + public Guid? AssignedUserId { get; set; } +} +```` + +对比`IssueCreationDto`,可以发现,缺少了`RepositoryId`属性,因为我们不允许跨仓库移动`Issue`.仅`Title`属性是必填的. + +`IssueAppService`中*Update*的实现如下:: + +````csharp +public class IssueAppService : ApplicationService, IIssueAppService +{ + private readonly IssueManager _issueManager; + private readonly IRepository _issueRepository; + private readonly IRepository _userRepository; + + public IssueAppService( + IssueManager issueManager, + IRepository issueRepository, + IRepository userRepository) + { + _issueManager = issueManager; + _issueRepository = issueRepository; + _userRepository = userRepository; + } + + public async Task UpdateAsync(Guid id, UpdateIssueDto input) + { + // Get entity from database + var issue = await _issueRepository.GetAsync(id); + + // Change Title + await _issueManager.ChangeTitleAsync(issue, input.Title); + + // Change Assigned User + if (input.AssignedUserId.HasValue) + { + var user = await _userRepository.GetAsync(input.AssignedUserId.Value); + await _issueManager.AssignToAsync(issue, user); + } + + // Change Text (no business rule, all values accepted) + issue.Text = input.Text; + + // Update entity in the database + await _issueRepository.UpdateAsync(issue); + + // Return a DTO represents the new Issue + return ObjectMapper.Map(issue); + } +} +```` + +* `UpdateAsync` 方法参数 `id`被作为独立参数,放置在`UpdateIssueDto`之外.这是一项设计决策,当你将此应用服务[自动导出](API/Auto-API-Controllers.md)为HTTP API时,API端点时帮助ABP正确定义HTTP路由,这与DDD无关. +* 首先从数据库中**获取** `Issue` 实体. +* 通过 `IssueManager`的 `ChangeTitleAsync`方法修改标题,而非直接通过 `Issue.SetTitle(...)`直接修改.因为我们需要像创建时那样,**执行标题的重复检查逻辑**.这需要对`Issue`类和`IssueManager`类进行一些调整(将在下面说明). +* 通过 `IssueManager`的 `AssignToAsync` 方法来**关联用户**. +* 直接设置 `Issue.Text`属性,因为它本身没有任何业务逻辑需要执行.如果以后需要可以再进行重构. +* **保存修改**至数据库.同样,保存修改后的实体属于应用服务的职责,它可以协调业务对象和事务.如果在`IssueManager`内部的 `ChangeTitleAsync` 和 `AssignToAsync` 方法中进行保存,则会导致两次数据库操作(请参见上面的*讨论:为什么`IssueManager`中没有执行`Issue`的保存?*) +* 最后,使用 `IObjectMapper` 将`Issue`实体**映射**为 `IssueDto` 并返回. + +如前所述,我们需要对`Issue`类和`IssueManager`类进行一些调整: + +首先,修改 `SetTitle`方法的访问级别为internal: + +````csharp +internal void SetTitle(string title) +{ + Title = Check.NotNullOrWhiteSpace(title, nameof(title)); +} +```` + +再在`IssueManager`中添加一个新方法来修改标题: + +````csharp +public async Task ChangeTitleAsync(Issue issue, string title) +{ + if (issue.Title == title) + { + return; + } + + if (await _issueRepository.AnyAsync(i => i.Title == title)) + { + throw new BusinessException("IssueTracking:IssueWithSameTitleExists"); + } + + issue.SetTitle(title); +} +```` + +## 领域逻辑和应用逻辑 + +如前所述,领域驱动设计中的*业务逻辑*分为两部分(各层):领域逻辑和应用逻辑 + +![domain-driven-design-domain-vs-application-logic](images/domain-driven-design-domain-vs-application-logic.png) + +领域逻辑是系统的*核心领域规则*组成,而应用逻辑则满足特定的*用例*. + +虽然定义很明确,但是实施起来缺并不容器.你可能无法确定哪些代码应该属于领域层,哪些代码应该属于应用层,本节会尝试解释差异. + +### 多应用层 + +当你的系统很大时,DDD有助于**处理复杂问题**.尤其是,**单个领域**需要多个**应用程序运行**,那么**领域逻辑与应用逻辑分离**就变的非常重要. + +假设你正字构建一个具有多个应用程序的系统: + +* 一个**公开的应用网站**,使用ASP.NET Core MVC构建,展示商品给来访者.这样的网站不选哟身份验证即可查看商品.来访者只有执行了某些操作(例如,将商品添加到购物车)后,才需要登录网站. +* 一个**后台管理系统**,UI使用Angular,通过REST API请求数据.内部员工使用这个系统来维护数据(例如,编辑商品说明). +* 一个**移动端应用程序**,它比公开的网站UI上更加简洁.它通过REST API或其它技术(例如,TCP sockets)请求数据. + +![domain-driven-design-multiple-applications](images/domain-driven-design-multiple-applications.png) + +每个应用程序都有不同的**需求**,不同的**用例**(应用服务方法),不同的DTO,不同的**验证**和**授权**规则等. + +将所有这些逻辑都集中到一个应用层中,会使你的服务包含太多的`if`条件分支及**复杂的业务逻辑**,从而使你的代码难道开发,**维护**,测试,引发各种问题. + +如果你在一个领域中有多个应用程序 + +- 为每种应用程序或客户端创建独立的应用层,并在这些单独层中执行特定于应用业务逻辑. +- 使用共享的核心领域逻辑 + +为了实现这样的设计,首先我们需要区分领域逻辑和应用逻辑. + +为了更清楚的实现,你可以为不同的应用类型创建不同的项目(`.csproj`): + +* `IssueTracker.Admin.Application` 和 `IssueTracker.Admin.Application.Contacts` 为后台管理系统提供服务. +* `IssueTracker.Public.Application` 和 `IssueTracker.Public.Application.Contracts` 为公开网站提供服务. +* `IssueTracker.Mobile.Application` 和 `IssueTracker.Mobile.Application.Contracts` 为移动端应用提供服务. + +### 示例 + +本节包含一些应用服务及领域服务的示例,讨论业务逻辑应该放置在哪一层 + +**示例:在领域服务中创建`Organization`** + +````csharp +public class OrganizationManager : DomainService +{ + private readonly IRepository _organizationRepository; + private readonly ICurrentUser _currentUser; + private readonly IAuthorizationService _authorizationService; + private readonly IEmailSender _emailSender; + + public OrganizationManager( + IRepository organizationRepository, + ICurrentUser currentUser, + IAuthorizationService authorizationService, + IEmailSender emailSender) + { + _organizationRepository = organizationRepository; + _currentUser = currentUser; + _authorizationService = authorizationService; + _emailSender = emailSender; + } + + public async Task CreateAsync(string name) + { + if (await _organizationRepository.AnyAsync(x => x.Name == name)) + { + throw new BusinessException("IssueTracking:DuplicateOrganizationName"); + } + + await _authorizationService.CheckAsync("OrganizationCreationPermission"); + + Logger.LogDebug($"Creating organization {name} by {_currentUser.UserName}"); + + var organization = new Organization(); + + await _emailSender.SendAsync( + "systemadmin@issuetracking.com", + "New Organization", + "A new organization created with name: " + name + ); + + return organization; + } +} +```` + +我们来逐个检查`CreateAsync`方法中的代码,讨论是否应该在领域服务中 + +* **正确**:首先检查有**无重复的组织名称**,并抛出异常.这与核心领域规则有关,因为我们绝对不允许重复的名称. +* **错误**:领域服务不应该执行**授权检查**,[授权](Authorization.md)应该在应用层处理. +* **错误**:它记录了日志,包括[当前用户](CurrentUser.md)的`UserName`.领域服务不应该依赖当前用户,即便系统中没有用户,领域服务也应可用.当前用户应该是与展现层或应用层有关的概念. +* **错误**:它发送了有关新组织被创建的[邮件](Emailing.md),我们认为这也是特定用例的业务逻辑,你可能像在不同的用例中创建不同的邮件,又或者某些情况无需发送邮件. + +**示例:应用服务中创建`Organization`** + +````csharp +public class OrganizationAppService : ApplicationService +{ + private readonly OrganizationManager _organizationManager; + private readonly IPaymentService _paymentService; + private readonly IEmailSender _emailSender; + + public OrganizationAppService( + OrganizationManager organizationManager, + IPaymentService paymentService, + IEmailSender emailSender) + { + _organizationManager = organizationManager; + _paymentService = paymentService; + _emailSender = emailSender; + } + + [UnitOfWork] + [Authorize("OrganizationCreationPermission")] + public async Task CreateAsync(CreateOrganizationDto input) + { + await _paymentService.ChargeAsync( + CurrentUser.Id, + GetOrganizationPrice() + ); + + var organization = await _organizationManager.CreateAsync(input.Name); + + await _organizationManager.InsertAsync(organization); + + await _emailSender.SendAsync( + "systemadmin@issuetracking.com", + "New Organization", + "A new organization created with name: " + input.Name + ); + + return organization; // !!! + } + + private double GetOrganizationPrice() + { + return 42; //Gets from somewhere else... + } +} +```` + +我们来逐个检查`CreateAsync`方法中的代码,讨论是否应该在应用服务中 + +* **正确**:应用服务的方法应该是一个工作单元(事务).ABP的[工作单元](Unit-Of-Work.md)系统可以使得此工作自动进行(甚至无需`[UnitOfWork]`注解). +* **正确**: [授权](Authorization.md)应该在应用层处理.这里通过使用`[Authorize]`来完成. +* **正确**:调用付款(基础设施服务)为此操作收取费用(创建组织是我们业务中的付费服务). +* **正确**:应用服务负责将变更的数据保存到数据库. +* **正确**:我们可以将[邮件](Emailing.md)作为通知发送给管理员. +* **错误**:请勿从应用服务中返回实体,应该返回DTO. + +**讨论:为什么不将支付逻辑移到领域服务中?** + +你可能想知道为什么付款逻辑代码不在`OrganizationManager`中.付款是非常**重要的事情**,我们不能**遗漏任何一次付款**. + +它确实非常重要,但是,它不能放到领域服务中.我们可能还有**其它用例**来创建组织但不收取任何费用.例如: + +* 管理员可以在后台管理系统创建新组织,而无需支付任何费用. +* 后台作业系统导入,集成,同步组织而无需支付费用. + +如你所见,**付款不是创建有效组织的必要操作**.它是特定的应用服务逻辑. + +**示例:CRUD操作** + +````csharp +public class IssueAppService +{ + private readonly IssueManager _issueManager; + + public IssueAppService(IssueManager issueManager) + { + _issueManager = issueManager; + } + + public async Task GetAsync(Guid id) + { + return await _issueManager.GetAsync(id); + } + + public async Task CreateAsync(IssueCreationDto input) + { + await _issueManager.CreateAsync(input); + } + + public async Task UpdateAsync(UpdateIssueDto input) + { + await _issueManager.UpdateAsync(input); + } + + public async Task DeleteAsync(Guid id) + { + await _issueManager.DeleteAsync(id); + } +} +```` + +该应用服务本身**不执行任何操作**,并将所有**操作转发给** *领域服务*.它甚至将DTO传递给`IssueManager` + +* 如果没有**任何业务逻辑**,只有简单的**CRUD**操作,**请勿**创建领域服务. +* **切勿**将**DTO**传递给领域服务,或从领域服务返回**DTO**. + +可以在应用服务中直接注入仓储,实现查询,创建,更新及删除操作.除非在这些操作过程中需要执行某些业务逻辑,在这种情况下,请创建领域服务. + +> 不要创建"将来可能需要"这种CRUD领域服务方法([YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it)),在需要时重构它并重构现有代码. 由于应用层优雅地抽象了领域层,因此重构过程不会影响UI层和其他客户端. + +## 相关书籍 + +如果你对领域驱动设计和构建大型系统有兴趣,建议将以下书籍作为参考书籍: + +* "*Domain Driven Design*" by Eric Evans +* "*Implementing Domain Driven Design*" by Vaughn Vernon +* "*Clean Architecture*" by Robert C. Martin \ No newline at end of file diff --git a/docs/zh-Hans/Entity-Framework-Core-Migrations.md b/docs/zh-Hans/Entity-Framework-Core-Migrations.md index c6f1086540..8708516f76 100644 --- a/docs/zh-Hans/Entity-Framework-Core-Migrations.md +++ b/docs/zh-Hans/Entity-Framework-Core-Migrations.md @@ -235,7 +235,7 @@ public static class BackgroundJobsDbContextModelCreatingExtensions } ```` -此u还获取选项用于更改此模块的数据库表前缀和模式,但在这里并不重要. +此扩展方法还提供了选项用于更改此模块的数据库表前缀和模式,但在这里并不重要. 最终的应用程序在 `MigrationsDbContext` 类中调用扩展方法, 因此它可以确定此 `MigrationsDbContext` 维护的数据库中包含哪些模块. 如果要创建第二个数据库并将某些模块表移动到第二个数据库,则需要有第二个`MigrationsDbContext` 类,该类仅调用相关模块的扩展方法. 下一部分将详细介绍该主题. @@ -588,7 +588,7 @@ public class IdentityRoleExtendingService : ITransientDependency ````json "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" } ```` @@ -596,10 +596,10 @@ public class IdentityRoleExtendingService : ITransientDependency ````json "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true", - "AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true", - "AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true", - "AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True", + "AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True", + "AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True", + "AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True" } ```` @@ -883,4 +883,4 @@ public class BookStoreDbMigratorModule : AbpModule ## 结论 -本文档说明了如何拆分数据库以及管理Entity Framework Core解决方案的数据库迁移. 简而言之,你需要为每个不同的数据库创建一个单独的迁移项目. \ No newline at end of file +本文档说明了如何拆分数据库以及管理Entity Framework Core解决方案的数据库迁移. 简而言之,你需要为每个不同的数据库创建一个单独的迁移项目. diff --git a/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md b/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md index fb8ad15787..56a37396ea 100644 --- a/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md +++ b/docs/zh-Hans/Entity-Framework-Core-Other-DBMS.md @@ -62,22 +62,25 @@ MySQL连接字符串与SQL Server连接字符串不同. 所以检查你的解决 通常需要更改 `.DbMigrator` 和 `.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构. -## 更改迁移DbContext +## 更改迁移DbContext Factory -MySQL DBMS与SQL Server有一些细微的差异. 某些模块数据库映射配置(尤其是字段长度)会导致MySQL出现问题. 例如某些[IdentityServer模块](Modules/IdentityServer.md)表就存在这样的问题,它提供了一个选项可以根据你的DBMS配置字段. +启动模板包含***YourProjectName*MigrationsDbContextFactory**类,这是EF Core控制台命令所必须的类(比如[Add-Migration](https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/#generating--running-migrations)和[Update-Database](https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/#generating--running-migrations)),在切换到MySql数据库时,我们同时也需要修改`DbContextOptionsBuilder` -启动模板包含*YourProjectName*MigrationsDbContext,它负责维护和迁移数据库架构. 此DbContext基本上调用依赖模块的扩展方法来配置其数据库表. +在 *YourProjectName*MigrationsDbContextFactory 类中找到以下代码: -打开 *YourProjectName*MigrationsDbContext 更改 `builder.ConfigureIdentityServer();` 行,如下所示: +````csharp +var builder = new DbContextOptionsBuilder() + .UseSqlServer(configuration.GetConnectionString("Default")); +```` + +将其替换为: ````csharp -builder.ConfigureIdentityServer(options => -{ - options.DatabaseProvider = EfCoreDatabaseProvider.MySql; -}); +var builder = new DbContextOptionsBuilder() + .UseMySql(configuration.GetConnectionString("Default")); ```` -然后 `ConfigureIdentityServer()` 方法会将字段长度设置为超过MySQL的限制. 如果在创建或执行数据库迁移时遇到任何问题请参考相关的模块文档. +如果在创建或执行数据库迁移时遇到任何问题,请参考相关模块文档 ## 重新生成迁移 @@ -105,5 +108,6 @@ builder.ConfigureIdentityServer(options => options.DatabaseProvider = EfCoreDatabaseProvider.MySql; }); ``` +v2.9+版本无需手动设置 ([版本历史](https://github.com/abpframework/abp/blob/dev/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/EntityFrameworkCore/IdentityServerModelBuilderConfigurationOptions.cs)) -相关讨论: https://github.com/abpframework/abp/issues/1920 \ No newline at end of file +相关讨论: https://github.com/abpframework/abp/issues/1920 diff --git a/docs/zh-Hans/Entity-Framework-Core.md b/docs/zh-Hans/Entity-Framework-Core.md index 211ec4d65f..b5252b86c0 100644 --- a/docs/zh-Hans/Entity-Framework-Core.md +++ b/docs/zh-Hans/Entity-Framework-Core.md @@ -263,7 +263,7 @@ context.Services.AddAbpDbContext(options => 在你想要覆盖默认仓储方法对其自定义时,这一点非常需要. 例如你可能希望自定义`DeleteAsync`方法覆盖默认实现 ````csharp -public override async Task DeleteAsync( +public async override Task DeleteAsync( Guid id, bool autoSave = false, CancellationToken cancellationToken = default) @@ -365,7 +365,7 @@ public class MyRepositoryBase : EfCoreRepository where TEntity : class, IEntity { - public MyRepositoryBase(IDbContextProvider dbContextProvider) + public MyRepositoryBase(IDbContextProvider dbContextProvider) : base(dbContextProvider) { } @@ -447,4 +447,4 @@ context.Services.AddAbpDbContext(options => ## 另请参阅 -* [实体](Entities.md) \ No newline at end of file +* [实体](Entities.md) diff --git a/docs/zh-Hans/Getting-Started-AspNetCore-Application.md b/docs/zh-Hans/Getting-Started-AspNetCore-Application.md index 0334609bd3..be5e50c2bd 100644 --- a/docs/zh-Hans/Getting-Started-AspNetCore-Application.md +++ b/docs/zh-Hans/Getting-Started-AspNetCore-Application.md @@ -1,194 +1,160 @@ -## 在AspNet Core MVC Web Application中使用ABP - -本教程将介绍如何开始以最少的依赖关系开始使用ABP开发. - -通常情况下你需要下载一个 ***[启动模板](Getting-Started-AspNetCore-MVC-Template.md)*** - -### 创建一个新项目 - -1. 使用Visual Studio创建一个空的AspNet Core Web Application: - -![](images/create-new-aspnet-core-application.png) - -2. 选择空模板 - -![](images/select-empty-web-application.png) - -你可以选择其它模板,但是我想要从一个简洁的项目演示它. - -### 安装 Volo.Abp.AspNetCore.Mvc 包 - -Volo.Abp.AspNetCore.Mvc是ABP集成AspNet Core MVC的包,请安装它到你项目中: - -```` -Install-Package Volo.Abp.AspNetCore.Mvc -```` - -### 创建ABP模块 - -ABP是一个模块化框架,它需要一个**启动 (根) 模块**继承自``AbpModule``: - -````C# -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Hosting; -using Volo.Abp; -using Volo.Abp.AspNetCore.Mvc; -using Volo.Abp.Modularity; - -namespace BasicAspNetCoreApplication -{ - [DependsOn(typeof(AbpAspNetCoreMvcModule))] - public class AppModule : AbpModule - { - public override void OnApplicationInitialization( - ApplicationInitializationContext context) - { - var app = context.GetApplicationBuilder(); - var env = context.GetEnvironment(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Error"); - } - - app.UseStaticFiles(); - app.UseRouting(); - app.UseConfiguredEndpoints(); - } - } -} -```` - -``AppModule`` 是应用程序启动模块的好名称(建议你的启动模块也使用这个命名). - -ABP的包定义了这个模块类,模块可以依赖其它模块.在上面的代码中 ``AppModule`` 依赖于 ``AbpAspNetCoreMvcModule`` (模块存在于Volo.Abp.AspNetCore.Mvc包中). 安装新的ABP的包后添加``DependsOn``是很常见的做法. - -我们在此模块类中配置ASP.NET Core管道,而不是Startup类中. - -### 启动类 - -接下来修改启动类集成ABP模块系统: - -````C# -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace BasicAspNetCoreApplication -{ - public class Startup - { - public IServiceProvider ConfigureServices(IServiceCollection services) - { - services.AddApplication(); - - return services.BuildServiceProviderFromFactory(); - } - - public void Configure(IApplicationBuilder app) - { - app.InitializeApplication(); - } - } -} - -```` - -修改``ConfigureServices``方法的返回值为``IServiceProvider``(默认是``void``).这个修改允许我们替换AspNet Core的依赖注入框架. (参阅下面的Autofac集成部分). ``services.AddApplication()``添加了所有模块中定义的全部服务. - -``app.InitializeApplication()`` 调用 ``Configure`` 方法初始化并启动应用程序 - -### Hello World! - -上面的应用程序没有什么功能,让我们创建一个MVC控制器实现一些功能: - -````C# -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; - -namespace BasicAspNetCoreApplication.Controllers -{ - public class HomeController : AbpController - { - public IActionResult Index() - { - return Content("Hello World!"); - } - } -} - -```` - -如果运行这个应用程序你会在页面中看到"Hello World!". - -Derived ``HomeController`` from ``AbpController`` instead of standard ``Controller`` class. This is not required, but ``AbpController`` class has useful base properties and methods to make your development easier. - -从``AbpController``派生``HomeController`` 而不是继承自``Controller``类.虽然这不是强制要求,但是``AbpController``类有很多有用的有属性和方法,使你的开发更容易. - -### 使用 Autofac 依赖注入框架 - -虽然AspNet Core的依赖注入(DI)系统适用于基本要求,但Autofac提供了属性注入和方法拦截等高级功能,这些功能是ABP执行高级应用程序框架功能所必需的. - -用Autofac取代AspNet Core的DI系统并集成到ABP非常简单. - -1. 安装 Volo.Abp.Autofac 包 - -```` -Install-Package Volo.Abp.Autofac -```` - -2. 添加 ``AbpAutofacModule`` 依赖 - -````C# -[DependsOn(typeof(AbpAspNetCoreMvcModule))] -[DependsOn(typeof(AbpAutofacModule))] // 在模块上添加依赖AbpAutofacModule -public class AppModule : AbpModule -{ - ... -} -```` - -3. 修改在``Startup``类下的``services.AddApplication();``如下所示: - -````C# -services.AddApplication(options => -{ - options.UseAutofac(); // 集成 Autofac -}); -```` - -4. 更新 `Program.cs`代码, 不再使用`WebHost.CreateDefaultBuilder()`方法(因为它使用默认的DI容器): - - ````csharp -public class Program -{ - public static void Main(string[] args) - { - /* - https://github.com/aspnet/AspNetCore/issues/4206#issuecomment-445612167 - CurrentDirectoryHelpers 文件位于: \framework\src\Volo.Abp.AspNetCore.Mvc\Microsoft\AspNetCore\InProcess\CurrentDirectoryHelpers.cs - 当升级到ASP.NET Core 3.0的时候将会删除这个类. - */ - CurrentDirectoryHelpers.SetCurrentDirectory(); - - BuildWebHostInternal(args).Run(); - } - public static IWebHost BuildWebHostInternal(string[] args) => - new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIIS() - .UseIISIntegration() - .UseStartup() - .Build(); -} -```` - - -### 源码 - -从[此处](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication)获取本教程中创建的示例项目的源代码. +# 在AspNet Core MVC Web Application中使用ABP + +本教程将介绍如何开始以最少的依赖关系开始使用ABP开发. + +通常情况下你需要下载一个 **[启动模板](Getting-Started-AspNetCore-MVC-Template.md)** + +## 创建一个新项目 + +1. 使用Visual Studio 2019 (16.4.0+)创建一个新的AspNet Core Web Application: + +![](images/create-new-aspnet-core-application-v2.png) + +2. 配置新的项目: + +![](images/select-empty-web-application-v2.png) + +3. 完成创建: + +![](images/create-aspnet-core-application.png) + + +## 安装 Volo.Abp.AspNetCore.Mvc 包 + +Volo.Abp.AspNetCore.Mvc是ABP集成AspNet Core MVC的包,请安装它到你项目中: + +```` +Install-Package Volo.Abp.AspNetCore.Mvc +```` + +## 创建ABP模块 + +ABP是一个模块化框架,它需要一个**启动 (根) 模块**继承自``AbpModule``: + +````C# +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Hosting; +using Volo.Abp; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Modularity; + +namespace BasicAspNetCoreApplication +{ + [DependsOn(typeof(AbpAspNetCoreMvcModule))] + public class AppModule : AbpModule + { + public override void OnApplicationInitialization( + ApplicationInitializationContext context) + { + var app = context.GetApplicationBuilder(); + var env = context.GetEnvironment(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Error"); + } + + app.UseStaticFiles(); + app.UseRouting(); + app.UseConfiguredEndpoints(); + } + } +} +```` + +``AppModule`` 是应用程序启动模块的好名称. + +ABP的包定义了这个模块类,模块可以依赖其它模块.在上面的代码中 ``AppModule`` 依赖于 ``AbpAspNetCoreMvcModule`` (模块存在于[Volo.Abp.AspNetCore.Mvc](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc)包中). 安装新的ABP的包后添加``DependsOn``是很常见的做法. + +我们在此模块类中配置ASP.NET Core管道,而不是Startup类中. + +### 启动类 + +接下来修改启动类集成ABP模块系统: + +````C# +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace BasicAspNetCoreApplication +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddApplication(); + } + + public void Configure(IApplicationBuilder app) + { + app.InitializeApplication(); + } + } +} + +```` + +``services.AddApplication()``添加了所有``AppModule``模块中定义的全部服务. + +``Configure``方法中的``app.InitializeApplication()``完成初始化并启动应用程序. + +## 运行应用程序! + +启动该应用,它将按预期运行. + +## 使用 Autofac 依赖注入框架 + +虽然AspNet Core的依赖注入(DI)系统适用于基本要求,但[Autofac](https://autofac.org/)提供了属性注入和方法拦截等高级功能,这些功能是ABP执行高级应用程序框架功能所必需的. + +用Autofac取代AspNet Core的DI系统并集成到ABP非常简单. + +1. 安装 [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) 包 + +```` +Install-Package Volo.Abp.Autofac +```` + +2. 添加 ``AbpAutofacModule`` 依赖 + +````C# +[DependsOn(typeof(AbpAspNetCoreMvcModule))] +[DependsOn(typeof(AbpAutofacModule))] // 在模块上添加依赖AbpAutofacModule +public class AppModule : AbpModule +{ + ... +} +```` + +3. 修改``Program.cs``以使用Autofac: + +````C# +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace BasicAspNetCoreApplication +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }) + .UseAutofac(); // 添加这一行 + } +} +```` + +## 源码 + +从[此处](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication)获取本教程中创建的示例项目的源代码. diff --git a/docs/zh-Hans/How-To/Customize-SignIn-Manager.md b/docs/zh-Hans/How-To/Customize-SignIn-Manager.md index 73cbcd2ce7..eea0364e6e 100644 --- a/docs/zh-Hans/How-To/Customize-SignIn-Manager.md +++ b/docs/zh-Hans/How-To/Customize-SignIn-Manager.md @@ -38,7 +38,7 @@ public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager GetExternalLoginInfoAsync(string expectedXsrf = null) +public async override Task GetExternalLoginInfoAsync(string expectedXsrf = null) { var auth = await Context.AuthenticateAsync(Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme); var items = auth?.Properties?.Items; diff --git a/docs/zh-Hans/Modules/Account.md b/docs/zh-Hans/Modules/Account.md new file mode 100644 index 0000000000..2e1c838f50 --- /dev/null +++ b/docs/zh-Hans/Modules/Account.md @@ -0,0 +1,36 @@ +# 账户模块 + +该模块提供必要的UI页面与组件使用户登录和注册到应用程序. + +> 文档正在更新 + +## 社交/外部登录 + +### 示例: Facebook 认证 + +按照[ASP.NET Core Facebook集成文档](https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/social/facebook-logins)向你应用程序添加Facebook登录. + +#### 添加NuGet包 + +添加[Microsoft.AspNetCore.Authentication.Facebook]包到你的项目. 基于你的架构,可能是 `.Web`,`.IdentityServer`(对于分层启动)或 `.Host` 项目. + +#### 配置提供程序 + +在你模块的 `ConfigureServices` 方法中使用 `.AddFacebook(...)` 扩展方法来配置客户端: + +````csharp +context.Services.AddAuthentication() + .AddFacebook(facebook => + { + facebook.AppId = "..."; + facebook.AppSecret = "..."; + facebook.Scope.Add("email"); + facebook.Scope.Add("public_profile"); + }); +```` + +> 最佳实践是使用 `appsettings.json` 或ASP.NET Core用户机密系统来存储你的凭据,而不是像这样硬编码值. 请参阅[微软](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins)文档了解如何使用用户机密. + +### Angular UI + +从v3.1开始,Angular UI使用授权码流程(作为最佳实践)通过重定向到MVC UI登录页面来对用户进行身份验证. 因此,即使你使用的是Angular UI,社交/外部登录集成也与上面说明的相同.并且可以开箱即用. \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Blogging.md b/docs/zh-Hans/Modules/Blogging.md new file mode 100644 index 0000000000..cb82a640d5 --- /dev/null +++ b/docs/zh-Hans/Modules/Blogging.md @@ -0,0 +1 @@ +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Client-Simulation.md b/docs/zh-Hans/Modules/Client-Simulation.md new file mode 100644 index 0000000000..bc5d38e814 --- /dev/null +++ b/docs/zh-Hans/Modules/Client-Simulation.md @@ -0,0 +1,3 @@ +# Client Simulation Module + +TODO \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Cms-Kit.md b/docs/zh-Hans/Modules/Cms-Kit.md new file mode 100644 index 0000000000..e13f1e585b --- /dev/null +++ b/docs/zh-Hans/Modules/Cms-Kit.md @@ -0,0 +1,3 @@ +# CMS Kit Module + +TODO \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Docs.md b/docs/zh-Hans/Modules/Docs.md index acc956be1b..1d09552e50 100644 --- a/docs/zh-Hans/Modules/Docs.md +++ b/docs/zh-Hans/Modules/Docs.md @@ -47,7 +47,7 @@ ABP框架的[文档](docs.abp.io)也是使用的此模块. ```json { "ConnectionStrings": { - "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True" } } ``` diff --git a/docs/zh-Hans/Modules/Feature-Management.md b/docs/zh-Hans/Modules/Feature-Management.md new file mode 100644 index 0000000000..cb82a640d5 --- /dev/null +++ b/docs/zh-Hans/Modules/Feature-Management.md @@ -0,0 +1 @@ +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Identity.md b/docs/zh-Hans/Modules/Identity.md index 5589fb367e..420b696a7c 100644 --- a/docs/zh-Hans/Modules/Identity.md +++ b/docs/zh-Hans/Modules/Identity.md @@ -2,7 +2,7 @@ 身份模块基于Microsoft Identity库用于管理[组织单元](Organization-Units.md), 角色, 用户和他们的权限. -参阅 [源码](https://github.com/abpframework/abp/tree/dev/modules/identity). 文档很快会被完善. +> 参阅 [源码](https://github.com/abpframework/abp/tree/dev/modules/identity). 文档很快会被完善. ## Identity安全日志 diff --git a/docs/zh-Hans/Modules/IdentityServer.md b/docs/zh-Hans/Modules/IdentityServer.md new file mode 100644 index 0000000000..cb82a640d5 --- /dev/null +++ b/docs/zh-Hans/Modules/IdentityServer.md @@ -0,0 +1 @@ +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/Modules/Index.md b/docs/zh-Hans/Modules/Index.md index dab2f838df..656e5db96f 100644 --- a/docs/zh-Hans/Modules/Index.md +++ b/docs/zh-Hans/Modules/Index.md @@ -1,6 +1,6 @@ # 应用程序模块 -ABP是一个 **模块化的应用程序框架** 由十多个 **nuget packages** 组成. 它提供了一个完整的基础设施来构建你自己的应用程序模块,这些模块包含实体,服务,数据库集成,API,UI组件等. +ABP是一个 **模块化的应用程序框架** 由十多个 **NuGet & NPM packages** 组成. 它提供了一个完整的基础设施来构建你自己的应用程序模块,这些模块包含实体,服务,数据库集成,API,UI组件等. **有两种类型的模块.** 它们没有任何结构上的差异,只是按照功能和目地分类: @@ -9,20 +9,20 @@ ABP是一个 **模块化的应用程序框架** 由十多个 **nuget packages** ## 开源的应用程序模块 -有一些由ABP社区开发和维护的 **开源免费** 的应用程序模块: +有一些由ABP框架开发和维护的 **开源免费** 的应用程序模块: -* **Account**: 提供账户管理UI,并允许用户登录/注册应用程序. +* [**Account**](Account.md): 提供账户管理UI,并允许用户登录/注册应用程序. * [**Audit Logging**](Audit-Logging.md): 用于将审计日志持久化到数据库. -* **Background Jobs**: 用于在使用默认后台作业管理器时保存后台作业. -* **Blogging**: 用于创建精美的博客. ABP的[博客](https://blog.abp.io/) 就使用了此模块. +* [**Background Jobs**](Background-Jobs.md): 用于在使用默认后台作业管理器时保存后台作业. +* [**Blogging**](Blogging.md): 用于创建精美的博客. ABP的[博客](https://blog.abp.io/) 就使用了此模块. * [**Docs**](Docs.md): 用于创建技术文档页面. ABP的[文档](https://abp.io/documents/) 就使用了此模块. -* **Feature Management**: 用于保存和管理功能. +* [**Feature Management**](Feature-Management.md): 用于保存和管理功能. * [**Identity**](Identity.md): 基于Microsoft Identity管理组织单元,角色,用户和他们的权限. -* **Identity Server**: 集成了IdentityServer4. -* **Permission Management**: 用于保存权限. -* **Setting Management**: 用于保存设置. -* **Tenant Management**: 管理[多租户](../Multi-Tenancy.md)应用程序的租户. -* **Users**: 抽象用户, 因此其他模块可以依赖此模块而不是Identity模块. +* [**Identity Server**](IdentityServer.md): 集成了IdentityServer4. +* [**Permission Management**](Permission-Management.md): 用于保存权限. +* [**Setting Management**](Setting-Management.md): 用于保存设置. +* [**Tenant Management**](Tenant-Management.md): 管理[多租户](../Multi-Tenancy.md)应用程序的租户. +* [**Users**](Users.md): 抽象用户, 因此其他模块可以依赖此模块而不是Identity模块. * [**Virtual File Explorer**](Virtual-File-Explorer.md): 提供简单的UI查看[虚拟文件系统](../Virtual-File-System.md)中的文件. 模块化文档正在编写中. 请参阅[这个仓库](https://github.com/abpframework/abp/tree/master/modules)获取所有模块的源代码. diff --git a/docs/zh-Hans/Modules/Users.md b/docs/zh-Hans/Modules/Users.md new file mode 100644 index 0000000000..cb82a640d5 --- /dev/null +++ b/docs/zh-Hans/Modules/Users.md @@ -0,0 +1 @@ +TODO... \ No newline at end of file diff --git a/docs/zh-Hans/MongoDB.md b/docs/zh-Hans/MongoDB.md index c90b9b5740..2ceeb1e00c 100644 --- a/docs/zh-Hans/MongoDB.md +++ b/docs/zh-Hans/MongoDB.md @@ -160,7 +160,7 @@ public interface IBookRepository : IRepository 实现`IBookRepository`接口的例子: ```csharp -public class BookRepository : +public class BookRepository : MongoDbRepository, IBookRepository { @@ -200,9 +200,9 @@ context.Services.AddMongoDbContext(options => 当你想**重写基础仓储方法**时,这一点尤为重要.例如,你想要重写`DeleteAsync`方法,以便更有效的删除实体: ```csharp -public override async Task DeleteAsync( - Guid id, - bool autoSave = false, +public async override Task DeleteAsync( + Guid id, + bool autoSave = false, CancellationToken cancellationToken = default) { //TODO: 自定义实现删除方法 @@ -338,4 +338,4 @@ context.Services.AddMongoDbContext(options => }); ``` -这个例子中,`OtherMongoDbContext`实现了`IBookStoreMongoDbContext`.这个特性允许你在发开的时候使用多个MongoDbContext(每个模块一个),但是运行的时候只能使有一个MongoDbContext(实现所有MongoDbContexts的所有接口) \ No newline at end of file +这个例子中,`OtherMongoDbContext`实现了`IBookStoreMongoDbContext`.这个特性允许你在发开的时候使用多个MongoDbContext(每个模块一个),但是运行的时候只能使有一个MongoDbContext(实现所有MongoDbContexts的所有接口) diff --git a/docs/zh-Hans/Samples/Microservice-Demo.md b/docs/zh-Hans/Samples/Microservice-Demo.md index 59977908a1..ca8b90a03f 100644 --- a/docs/zh-Hans/Samples/Microservice-Demo.md +++ b/docs/zh-Hans/Samples/Microservice-Demo.md @@ -843,7 +843,7 @@ Swagger UI已配置,是此服务的默认页面. 如果你导航到URL`http://lo ````json "ConnectionStrings": { - "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true", + "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True", "Blogging": "mongodb://localhost/MsDemo_Blogging" } ```` @@ -969,8 +969,8 @@ public class ProductServiceMigrationDbContext : AbpDbContext NuGet包管理器*下的**程序包管理控制台 (PMC)**. +启动模板使用[EF Core Code First Migrations](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/)创建和维护数据库架构. 打开菜单*工具 > NuGet包管理器*下的**程序包管理控制台 (PMC)**. ![Open Package Manager Console](images/bookstore-open-package-manager-console.png) @@ -201,7 +201,7 @@ Add-Migration "Created_Book_Entity" 在更新数据库之前,请阅读下面的部分了解如何将一些初始数据插入到数据库. -> 如果你使用其他IDE而不是Visual Studio, 你可以使用 [`dotnet-ef]`(https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli#create-a-migration) 工具. +> 如果你使用其他IDE而不是Visual Studio, 你可以使用 [`dotnet-ef`](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli#create-a-migration) 工具. {{end}} @@ -262,7 +262,7 @@ namespace Acme.BookStore } ``` -* 如果数据库中当前没有图书,则此代码使用 `IRepository`(默认为[repository](../Repositories.md)将两本书插入数据库. +* 如果数据库中当前没有图书,则此代码使用 `IRepository`(默认为[repository](../Repositories.md))将两本书插入数据库. ### 更新数据库 @@ -276,7 +276,7 @@ namespace Acme.BookStore 应用程序层由两个分离的项目组成: -* `Acme.BookStore.Application.Contracts 包含你的[DTO](../Data-Transfer-Objects.md)和[应用服务](../Application-Services.md)接口. +* `Acme.BookStore.Application.Contracts` 包含你的[DTO](../Data-Transfer-Objects.md)和[应用服务](../Application-Services.md)接口. * `Acme.BookStore.Application` 包含你的应用服务实现. 在本部分中,你将创建一个应用程序服务,使用ABP Framework的 `CrudAppService` 基类来获取,创建,更新和删除书籍. diff --git a/docs/zh-Hans/UI/Angular/Component-Replacement.md b/docs/zh-Hans/UI/Angular/Component-Replacement.md index 221ac6775a..2c37a5a2e7 100644 --- a/docs/zh-Hans/UI/Angular/Component-Replacement.md +++ b/docs/zh-Hans/UI/Angular/Component-Replacement.md @@ -49,7 +49,7 @@ export class AppComponent { yarn ng generate component my-application-layout ``` -在你的布局模板(`my-layout.component.html`)中添加以下代码: +在你的布局模板(`my-application-layout.component.html`)中添加以下代码: ```html @@ -543,4 +543,4 @@ export class AppComponent implements OnInit { ## 下一步是什么? -- [自定义设置页面](./Custom-Setting-Page.md) \ No newline at end of file +- [自定义设置页面](./Custom-Setting-Page.md) diff --git a/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md b/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md index 6a229714c1..dd688f256c 100644 --- a/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md +++ b/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md @@ -167,7 +167,27 @@ public class MyWebExtensionModule : AbpModule } ```` -> 无法通过代码配置未命名的bundle tag helpers, 因为它们的名称在开发时是未知的. 建议始终使用bundle tag helper的名称. +你也可以使用 `ConfigureAll` 方法配置所有现有的捆绑包: + +````C# +[DependsOn(typeof(MyWebModule))] +public class MyWebExtensionModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options + .ScriptBundles + .ConfigureAll(bundle => { + bundle.AddFiles( + "/scripts/my-extension-script.js" + ); + }); + }); + } +} +```` ### Bundle 贡献者 diff --git a/docs/zh-Hans/UI/AspNetCore/Customization-User-Interface.md b/docs/zh-Hans/UI/AspNetCore/Customization-User-Interface.md index c94c29d27f..44eb9b6e26 100644 --- a/docs/zh-Hans/UI/AspNetCore/Customization-User-Interface.md +++ b/docs/zh-Hans/UI/AspNetCore/Customization-User-Interface.md @@ -36,7 +36,7 @@ namespace Acme.BookStore.Web.Pages.Identity.Users { } - public override async Task OnPostAsync() + public async override Task OnPostAsync() { //TODO: Additional logic await base.OnPostAsync(); @@ -83,10 +83,10 @@ namespace Acme.BookStore.Web.Pages.Identity.Users public class MyLoginModel : LoginModel { public MyLoginModel( - IAuthenticationSchemeProvider schemeProvider, + IAuthenticationSchemeProvider schemeProvider, IOptions accountOptions ) : base( - schemeProvider, + schemeProvider, accountOptions) { diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Modals.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Modals.md index 28b92254bd..3abebdde0c 100644 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Modals.md +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Modals.md @@ -9,7 +9,7 @@ ````xml Launch modal - + Woohoo, you're reading this text in a modal! @@ -31,6 +31,13 @@ * `false` (默认值) * `true` +### Scrollable + +指定模态框滚动. 应为以下值之一: + +* `false` (默认值) +* `true` + ### size 指定模态框的大小. 应为以下值之一: @@ -76,4 +83,4 @@ * `Center` * `Around` * `Between` -* `End` \ No newline at end of file +* `End` diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index 1f9f6ce9c8..5d231099c9 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -21,9 +21,33 @@ } ] }, + { + "text": "启动模板", + "items": [ + { + "text": "概述", + "path": "Startup-Templates/Index.md" + }, + { + "text": "应用程序", + "path": "Startup-Templates/Application.md" + }, + { + "text": "模块", + "path": "Startup-Templates/Module.md" + }, + { + "text": "控制台", + "path": "Startup-Templates/Console.md" + }, + { + "text": "WPF", + "path": "Startup-Templates/WPF.md" + } + ] + }, { "text": "教程", - "path": "Tutorials/Index.md", "items": [ { "text": "应用开发", @@ -41,29 +65,10 @@ "path": "Tutorials/Part-3.md" } ] - } - ] - }, - { - "text": "指南", - "items": [ + }, { - "text": "自定义应用模块", - "path": "Customizing-Application-Modules-Guide.md", - "items": [ - { - "text": "扩展实体", - "path": "Customizing-Application-Modules-Extending-Entities.md" - }, - { - "text": "重写服务", - "path": "Customizing-Application-Modules-Overriding-Services.md" - }, - { - "text": "重写用户界面", - "path": "Customizing-Application-Modules-Overriding-User-Interface.md" - } - ] + "text": "社区文章", + "path": "https://community.abp.io/articles" }, { "text": "从ASP.NET Boilerplate迁移", @@ -71,19 +76,6 @@ } ] }, - { - "text": "CLI", - "path": "CLI.md" - }, - { - "text": "认证", - "items": [ - { - "text": "社交/外部登录", - "path": "Authentication/Social-External-Logins.md" - } - ] - }, { "text": "基础知识", "items": [ @@ -105,10 +97,6 @@ } ] }, - { - "text": "虚拟文件系统", - "path": "Virtual-File-System.md" - }, { "text": "本地化", "path": "Localization.md" @@ -139,17 +127,13 @@ "text": "日志", "path": "Logging.md" }, - { - "text": "审计日志", - "path": "Audit-Logging.md" - }, { "text": "设置管理", "path": "Settings.md" }, { - "text": "数据过滤", - "path": "Data-Filtering.md" + "text": "连接字符串", + "path": "Connection-Strings.md" }, { "text": "对象扩展", @@ -158,39 +142,88 @@ ] }, { - "text": "事件总线", - "items": [ - { - "text": "概述", - "path": "Event-Bus.md" - }, + "text": "基础设施", + "items":[ { - "text": "本地 Event Bus", - "path": "Local-Event-Bus.md" + "text": "后台服务", + "items": [ + { + "text": "后台作业", + "path": "Background-Jobs.md", + "items": [ + { + "text": "Hangfire 集成", + "path": "Background-Jobs-Hangfire.md" + }, + { + "text": "RabbitMQ 集成", + "path": "Background-Jobs-RabbitMq.md" + }, + { + "text": "Quartz 集成", + "path": "Background-Jobs-Quartz.md" + } + ] + }, + { + "text": "后台工作者", + "path": "Background-Workers.md", + "items": [ + { + "text": "Quartz 集成", + "path": "Background-Workers-Quartz.md" + } + ] + } + ] }, { - "text": "分布式 Event Bus", - "path": "Distributed-Event-Bus.md", + "text": "事件总线", "items": [ { - "text": "RabbitMQ 集成", - "path": "Distributed-Event-Bus-RabbitMQ-Integration.md" + "text": "概述", + "path": "Event-Bus.md" }, { - "text": "Kafka 集成", - "path": "Distributed-Event-Bus-Kafka-Integration.md" + "text": "本地 Event Bus", + "path": "Local-Event-Bus.md" }, { - "text": "Rebus 集成", - "path": "Distributed-Event-Bus-Rebus-Integration.md" + "text": "分布式 Event Bus", + "path": "Distributed-Event-Bus.md", + "items": [ + { + "text": "RabbitMQ 集成", + "path": "Distributed-Event-Bus-RabbitMQ-Integration.md" + }, + { + "text": "Kafka 集成", + "path": "Distributed-Event-Bus-Kafka-Integration.md" + }, + { + "text": "Rebus 集成", + "path": "Distributed-Event-Bus-Rebus-Integration.md" + } + ] } ] - } - ] - }, - { - "text": "服务", - "items": [ + }, + { + "text": "种子数据", + "path": "Data-Seeding.md" + }, + { + "text": "虚拟文件系统", + "path": "Virtual-File-System.md" + }, + { + "text": "审计日志", + "path": "Audit-Logging.md" + }, + { + "text": "数据过滤", + "path": "Data-Filtering.md" + }, { "text": "当前用户", "path": "CurrentUser.md" @@ -270,71 +303,102 @@ ] }, { - "text": "多租户", - "path": "Multi-Tenancy.md" - }, - { - "text": "模块开发", - "items": [ - { - "text": "基础", - "path": "Module-Development-Basics.md" - }, - { - "text": "模块插件" - }, + "text": "架构", + "items":[ { - "text": "最佳实践", - "path": "Best-Practices/Index.md" - } - ] - }, - { - "text": "领域驱动设计", - "items": [ - { - "text": "概述", - "path": "Domain-Driven-Design.md" - }, - { - "text": "领域层", + "text": "模块化", "items": [ { - "text": "实体&聚合根", - "path": "Entities.md" - }, - { - "text": "值对象" + "text": "基础", + "path": "Module-Development-Basics.md" }, { - "text": "仓储", - "path": "Repositories.md" + "text": "模块插件" }, { - "text": "领域服务" + "text": "自定义应用模块", + "path": "Customizing-Application-Modules-Guide.md", + "items": [ + { + "text": "扩展实体", + "path": "Customizing-Application-Modules-Extending-Entities.md" + }, + { + "text": "重写服务", + "path": "Customizing-Application-Modules-Overriding-Services.md" + }, + { + "text": "重写用户界面", + "path": "Customizing-Application-Modules-Overriding-User-Interface.md" + } + ] }, { - "text": "规约", - "path": "Specifications.md" + "text": "最佳实践", + "path": "Best-Practices/Index.md" } ] }, { - "text": "应用服务层", + "text": "领域驱动设计", "items": [ { - "text": "应用服务", - "path": "Application-Services.md" + "text": "概述", + "path": "Domain-Driven-Design.md" + }, + { + "text": "领域层", + "items": [ + { + "text": "实体&聚合根", + "path": "Entities.md" + }, + { + "text": "值对象" + }, + { + "text": "仓储", + "path": "Repositories.md" + }, + { + "text": "领域服务" + }, + { + "text": "规约", + "path": "Specifications.md" + } + ] }, { - "text": "数据传输对象(DTO)", - "path": "Data-Transfer-Objects.md" + "text": "应用服务层", + "items": [ + { + "text": "应用服务", + "path": "Application-Services.md" + }, + { + "text": "数据传输对象(DTO)", + "path": "Data-Transfer-Objects.md" + }, + { + "text": "工作单元", + "path": "Unit-Of-Work.md" + } + ] }, { - "text": "工作单元", - "path": "Unit-Of-Work.md" + "text": "指南: 实现DDD", + "path": "Domain-Driven-Design-Implementation-Guide.md" } ] + }, + { + "text": "多租户", + "path": "Multi-Tenancy.md" + }, + { + "text": "微服务架构", + "path": "Microservice-Architecture.md" } ] }, @@ -516,58 +580,45 @@ "path": "Data-Access.md" }, { - "text": "连接字符串", - "path": "Connection-Strings.md" - }, - { - "text": "数据库提供程序", + "text": "Entity Framework Core", + "path": "Entity-Framework-Core.md", "items": [ { - "text": "Entity Framework Core", - "path": "Entity-Framework-Core.md", - "items": [ + "text": "数据库迁移", + "path": "Entity-Framework-Core-Migrations.md" + }, + { + + "text": "切换DMBS", + "path": "Entity-Framework-Core-Other-DBMS.md", + "items":[ + { + "text": "到MySql", + "path": "Entity-Framework-Core-MySQL.md" + }, + { + "text": "到PostgreSQL", + "path": "Entity-Framework-Core-PostgreSQL.md" + }, { - "text": "数据库迁移", - "path": "Entity-Framework-Core-Migrations.md" + "text": " Oracle", + "path": "Entity-Framework-Core-Oracle.md" }, { - - "text": "切换DMBS", - "path": "Entity-Framework-Core-Other-DBMS.md", - "items":[ - { - "text": "到MySql", - "path": "Entity-Framework-Core-MySQL.md" - }, - { - "text": "到PostgreSQL", - "path": "Entity-Framework-Core-PostgreSQL.md" - }, - { - "text": " Oracle", - "path": "Entity-Framework-Core-Oracle.md" - }, - { - "text": "到SQLite", - "path": "Entity-Framework-Core-SQLite.md" - } - ] + "text": "到SQLite", + "path": "Entity-Framework-Core-SQLite.md" } ] - }, - { - "text": "MongoDB", - "path": "MongoDB.md" - }, - { - "text": "Dapper", - "path": "Dapper.md" } ] }, { - "text": "种子数据", - "path": "Data-Seeding.md" + "text": "MongoDB", + "path": "MongoDB.md" + }, + { + "text": "Dapper", + "path": "Dapper.md" } ] }, @@ -581,111 +632,128 @@ ] }, { - "text": "后台服务", + "text": "示例", "items": [ { - "text": "后台作业", - "path": "Background-Jobs.md", - "items": [ - { - "text": "Hangfire 集成", - "path": "Background-Jobs-Hangfire.md" - }, - { - "text": "RabbitMQ 集成", - "path": "Background-Jobs-RabbitMq.md" - }, - { - "text": "Quartz 集成", - "path": "Background-Jobs-Quartz.md" - } - ] + "text": "所有示例", + "path": "Samples/Index.md" }, { - "text": "后台工作者", - "path": "Background-Workers.md", - "items": [ - { - "text": "Quartz 集成", - "path": "Background-Workers-Quartz.md" - } - ] + "text": "微服务示例", + "path": "Samples/Microservice-Demo.md" } ] - }, + }, { - "text": "启动模板", - "items": [ + "text": "应用模块", + "items":[ { "text": "概述", - "path": "Startup-Templates/Index.md" + "path": "Modules/Index.md" }, { - "text": "应用程序", - "path": "Startup-Templates/Application.md" + "text": "账户", + "path": "Modules/Account.md" }, { - "text": "模块", - "path": "Startup-Templates/Module.md" + "text": "审计日志", + "path": "Modules/Audit-Logging.md" }, { - "text": "控制台", - "path": "Startup-Templates/Console.md" + "text": "后台作业", + "path": "Modules/Background-Jobs.md" }, { - "text": "WPF", - "path": "Startup-Templates/WPF.md" + "text": "博客", + "path": "Modules/Blogging.md" + }, + { + "text": "客户端模拟", + "path": "Modules/Client-Simulation.md" + }, + { + "text": "CMS Kit", + "path": "Modules/Cms-Kit.md" + }, + { + "text": "文档", + "path": "Modules/Docs.md" + }, + { + "text": "功能管理", + "path": "Modules/Feature-Management.md" + }, + { + "text": "Identity", + "path": "Modules/Identity.md" + }, + { + "text": "IdentityServer", + "path": "Modules/IdentityServer.md" + }, + { + "text": "权限管理", + "path": "Modules/Permission-Management.md" + }, + { + "text": "设置管理", + "path": "Modules/Setting-Management.md" + }, + { + "text": "租户管理", + "path": "Modules/Tenant-Management.md" + }, + { + "text": "用户", + "path": "Modules/Users.md" + }, + { + "text": "虚拟文件浏览器", + "path": "Modules/Virtual-File-Explorer.md" } ] }, { - "text": "示例", + "text": "发布信息", "items": [ { - "text": "所有示例", - "path": "Samples/Index.md" + "text": "升级", + "path": "Upgrading.md" }, { - "text": "微服务示例", - "path": "Samples/Microservice-Demo.md" + "text": "官方包", + "path": "https://abp.io/packages" + }, + { + "text": "预览版本", + "path": "Previews.md" + }, + { + "text": "每日构建", + "path": "Nightly-Builds.md" + }, + { + "text": "路线图", + "path": "Road-Map.md" } ] }, { - "text": "应用模块", - "path": "Modules/Index.md" - }, - { - "text": "微服务架构", - "path": "Microservice-Architecture.md" - }, - { - "text": "预览版本", - "path": "Previews.md" - }, - { - "text": "每日构建", - "path": "Nightly-Builds.md" - }, - { - "text": "路线图", - "path": "Road-Map.md" - }, - { - "text": "升级", - "path": "Upgrading.md" + "text": "参考", + "items": [ + { + "text": "CLI", + "path": "CLI.md" + }, + { + "text": "API文档", + "path": "{ApiDocumentationUrl}" + } + ] }, { "text": "贡献指南", "path": "Contribution/Index.md" - }, - { - "text": "API文档", - "path": "{ApiDocumentationUrl}" - }, - { - "text": "官方包", - "path": "https://abp.io/packages" } ] } diff --git a/docs/zh-Hans/images/abp-dynamic-form-result.png b/docs/zh-Hans/images/abp-dynamic-form-result.png new file mode 100644 index 0000000000..f9ecf14bd0 Binary files /dev/null and b/docs/zh-Hans/images/abp-dynamic-form-result.png differ diff --git a/docs/zh-Hans/images/abp-form-input-validation-error-french-name.png b/docs/zh-Hans/images/abp-form-input-validation-error-french-name.png new file mode 100644 index 0000000000..5112cd2597 Binary files /dev/null and b/docs/zh-Hans/images/abp-form-input-validation-error-french-name.png differ diff --git a/docs/zh-Hans/images/abp-form-input-validation-error-french.png b/docs/zh-Hans/images/abp-form-input-validation-error-french.png new file mode 100644 index 0000000000..cb8316d04b Binary files /dev/null and b/docs/zh-Hans/images/abp-form-input-validation-error-french.png differ diff --git a/docs/zh-Hans/images/abp-form-input-validation-error.png b/docs/zh-Hans/images/abp-form-input-validation-error.png new file mode 100644 index 0000000000..2c60adc343 Binary files /dev/null and b/docs/zh-Hans/images/abp-form-input-validation-error.png differ diff --git a/docs/zh-Hans/images/ajax-error.png b/docs/zh-Hans/images/ajax-error.png new file mode 100644 index 0000000000..2b9bc2bb37 Binary files /dev/null and b/docs/zh-Hans/images/ajax-error.png differ diff --git a/docs/zh-Hans/images/aspnetcore-web-tests-in-solution.png b/docs/zh-Hans/images/aspnetcore-web-tests-in-solution.png new file mode 100644 index 0000000000..12fca58139 Binary files /dev/null and b/docs/zh-Hans/images/aspnetcore-web-tests-in-solution.png differ diff --git a/docs/zh-Hans/images/basic-theme-account-layout.png b/docs/zh-Hans/images/basic-theme-account-layout.png new file mode 100644 index 0000000000..bccd0e20cc Binary files /dev/null and b/docs/zh-Hans/images/basic-theme-account-layout.png differ diff --git a/docs/zh-Hans/images/basic-theme-application-layout-blazor.png b/docs/zh-Hans/images/basic-theme-application-layout-blazor.png new file mode 100644 index 0000000000..f01becad07 Binary files /dev/null and b/docs/zh-Hans/images/basic-theme-application-layout-blazor.png differ diff --git a/docs/zh-Hans/images/basic-theme-application-layout-parts.png b/docs/zh-Hans/images/basic-theme-application-layout-parts.png new file mode 100644 index 0000000000..5e8f9147a9 Binary files /dev/null and b/docs/zh-Hans/images/basic-theme-application-layout-parts.png differ diff --git a/docs/zh-Hans/images/basic-theme-application-layout.png b/docs/zh-Hans/images/basic-theme-application-layout.png new file mode 100644 index 0000000000..353e294056 Binary files /dev/null and b/docs/zh-Hans/images/basic-theme-application-layout.png differ diff --git a/docs/zh-Hans/images/blazor-message-confirm.png b/docs/zh-Hans/images/blazor-message-confirm.png new file mode 100644 index 0000000000..fe03620283 Binary files /dev/null and b/docs/zh-Hans/images/blazor-message-confirm.png differ diff --git a/docs/zh-Hans/images/blazor-message-error.png b/docs/zh-Hans/images/blazor-message-error.png new file mode 100644 index 0000000000..5192eae1e9 Binary files /dev/null and b/docs/zh-Hans/images/blazor-message-error.png differ diff --git a/docs/zh-Hans/images/blazor-message-success.png b/docs/zh-Hans/images/blazor-message-success.png new file mode 100644 index 0000000000..592fac8b7d Binary files /dev/null and b/docs/zh-Hans/images/blazor-message-success.png differ diff --git a/docs/zh-Hans/images/blazor-notification-bell-component.png b/docs/zh-Hans/images/blazor-notification-bell-component.png new file mode 100644 index 0000000000..65934ec24b Binary files /dev/null and b/docs/zh-Hans/images/blazor-notification-bell-component.png differ diff --git a/docs/zh-Hans/images/blazor-notification-success.png b/docs/zh-Hans/images/blazor-notification-success.png new file mode 100644 index 0000000000..d68dcf47d8 Binary files /dev/null and b/docs/zh-Hans/images/blazor-notification-success.png differ diff --git a/docs/zh-Hans/images/blazor-page-alert-example.png b/docs/zh-Hans/images/blazor-page-alert-example.png new file mode 100644 index 0000000000..799826e57f Binary files /dev/null and b/docs/zh-Hans/images/blazor-page-alert-example.png differ diff --git a/docs/zh-Hans/images/bookstore-branding-blazor.png b/docs/zh-Hans/images/bookstore-branding-blazor.png new file mode 100644 index 0000000000..74ddf35f0f Binary files /dev/null and b/docs/zh-Hans/images/bookstore-branding-blazor.png differ diff --git a/docs/zh-Hans/images/bookstore-logo-blazor.png b/docs/zh-Hans/images/bookstore-logo-blazor.png new file mode 100644 index 0000000000..4e01569813 Binary files /dev/null and b/docs/zh-Hans/images/bookstore-logo-blazor.png differ diff --git a/docs/zh-Hans/images/branding-appname.png b/docs/zh-Hans/images/branding-appname.png new file mode 100644 index 0000000000..9300ad2c7f Binary files /dev/null and b/docs/zh-Hans/images/branding-appname.png differ diff --git a/docs/zh-Hans/images/branding-nobrand.png b/docs/zh-Hans/images/branding-nobrand.png new file mode 100644 index 0000000000..1fdf78472b Binary files /dev/null and b/docs/zh-Hans/images/branding-nobrand.png differ diff --git a/docs/zh-Hans/images/breadcrumbs-example.png b/docs/zh-Hans/images/breadcrumbs-example.png new file mode 100644 index 0000000000..17ae5f8ba5 Binary files /dev/null and b/docs/zh-Hans/images/breadcrumbs-example.png differ diff --git a/docs/zh-Hans/images/datatables-custom-render-date.png b/docs/zh-Hans/images/datatables-custom-render-date.png new file mode 100644 index 0000000000..344237aea7 Binary files /dev/null and b/docs/zh-Hans/images/datatables-custom-render-date.png differ diff --git a/docs/zh-Hans/images/datatables-default-render-date.png b/docs/zh-Hans/images/datatables-default-render-date.png new file mode 100644 index 0000000000..430c889d2f Binary files /dev/null and b/docs/zh-Hans/images/datatables-default-render-date.png differ diff --git a/docs/zh-Hans/images/datatables-example.png b/docs/zh-Hans/images/datatables-example.png new file mode 100644 index 0000000000..ef3e74576b Binary files /dev/null and b/docs/zh-Hans/images/datatables-example.png differ diff --git a/docs/zh-Hans/images/datatables-row-actions-confirmation.png b/docs/zh-Hans/images/datatables-row-actions-confirmation.png new file mode 100644 index 0000000000..9be8433f9c Binary files /dev/null and b/docs/zh-Hans/images/datatables-row-actions-confirmation.png differ diff --git a/docs/zh-Hans/images/datatables-row-actions-icon.png b/docs/zh-Hans/images/datatables-row-actions-icon.png new file mode 100644 index 0000000000..70111fc1ac Binary files /dev/null and b/docs/zh-Hans/images/datatables-row-actions-icon.png differ diff --git a/docs/zh-Hans/images/datatables-row-actions.png b/docs/zh-Hans/images/datatables-row-actions.png new file mode 100644 index 0000000000..4eae0f098a Binary files /dev/null and b/docs/zh-Hans/images/datatables-row-actions.png differ diff --git a/docs/zh-Hans/images/db-options.png b/docs/zh-Hans/images/db-options.png new file mode 100644 index 0000000000..d9dc2ed0f4 Binary files /dev/null and b/docs/zh-Hans/images/db-options.png differ diff --git a/docs/zh-Hans/images/ddd-microservice-simple.png b/docs/zh-Hans/images/ddd-microservice-simple.png new file mode 100644 index 0000000000..1217b243c8 Binary files /dev/null and b/docs/zh-Hans/images/ddd-microservice-simple.png differ diff --git a/docs/zh-Hans/images/domain-driven-design-aggregate-keep-small.png b/docs/zh-Hans/images/domain-driven-design-aggregate-keep-small.png new file mode 100644 index 0000000000..56f805ebbe Binary files /dev/null and b/docs/zh-Hans/images/domain-driven-design-aggregate-keep-small.png differ diff --git a/docs/zh-Hans/images/domain-driven-design-clean-architecture.png b/docs/zh-Hans/images/domain-driven-design-clean-architecture.png new file mode 100644 index 0000000000..4ab6d2c93b Binary files /dev/null and b/docs/zh-Hans/images/domain-driven-design-clean-architecture.png differ diff --git a/docs/zh-Hans/images/domain-driven-design-domain-vs-application-logic.png b/docs/zh-Hans/images/domain-driven-design-domain-vs-application-logic.png new file mode 100644 index 0000000000..e370cba71e Binary files /dev/null and b/docs/zh-Hans/images/domain-driven-design-domain-vs-application-logic.png differ diff --git a/docs/zh-Hans/images/domain-driven-design-entity-primary-keys.png b/docs/zh-Hans/images/domain-driven-design-entity-primary-keys.png new file mode 100644 index 0000000000..7ee0096222 Binary files /dev/null and b/docs/zh-Hans/images/domain-driven-design-entity-primary-keys.png differ diff --git a/docs/zh-Hans/images/domain-driven-design-example-domain-schema.png b/docs/zh-Hans/images/domain-driven-design-example-domain-schema.png new file mode 100644 index 0000000000..fe3075b141 Binary files /dev/null and b/docs/zh-Hans/images/domain-driven-design-example-domain-schema.png differ diff --git a/docs/zh-Hans/images/domain-driven-design-issue-aggregate-diagram.png b/docs/zh-Hans/images/domain-driven-design-issue-aggregate-diagram.png new file mode 100644 index 0000000000..244564126a Binary files /dev/null and b/docs/zh-Hans/images/domain-driven-design-issue-aggregate-diagram.png differ diff --git a/docs/zh-Hans/images/domain-driven-design-layers.png b/docs/zh-Hans/images/domain-driven-design-layers.png new file mode 100644 index 0000000000..f2fc9af097 Binary files /dev/null and b/docs/zh-Hans/images/domain-driven-design-layers.png differ diff --git a/docs/zh-Hans/images/domain-driven-design-multiple-applications.png b/docs/zh-Hans/images/domain-driven-design-multiple-applications.png new file mode 100644 index 0000000000..6db4fc6443 Binary files /dev/null and b/docs/zh-Hans/images/domain-driven-design-multiple-applications.png differ diff --git a/docs/zh-Hans/images/domain-driven-design-project-relations.png b/docs/zh-Hans/images/domain-driven-design-project-relations.png new file mode 100644 index 0000000000..b7878d1385 Binary files /dev/null and b/docs/zh-Hans/images/domain-driven-design-project-relations.png differ diff --git a/docs/zh-Hans/images/domain-driven-design-reference-by-id-sample.png b/docs/zh-Hans/images/domain-driven-design-reference-by-id-sample.png new file mode 100644 index 0000000000..48a1913654 Binary files /dev/null and b/docs/zh-Hans/images/domain-driven-design-reference-by-id-sample.png differ diff --git a/docs/zh-Hans/images/domain-driven-design-vs-solution.png b/docs/zh-Hans/images/domain-driven-design-vs-solution.png new file mode 100644 index 0000000000..ef0f51a418 Binary files /dev/null and b/docs/zh-Hans/images/domain-driven-design-vs-solution.png differ diff --git a/docs/zh-Hans/images/domain-driven-design-web-request-flow.png b/docs/zh-Hans/images/domain-driven-design-web-request-flow.png new file mode 100644 index 0000000000..a9cb98e01f Binary files /dev/null and b/docs/zh-Hans/images/domain-driven-design-web-request-flow.png differ diff --git a/docs/zh-Hans/images/example-global-styles.png b/docs/zh-Hans/images/example-global-styles.png new file mode 100644 index 0000000000..04232aa62d Binary files /dev/null and b/docs/zh-Hans/images/example-global-styles.png differ diff --git a/docs/zh-Hans/images/features-action.png b/docs/zh-Hans/images/features-action.png new file mode 100644 index 0000000000..8283a95be9 Binary files /dev/null and b/docs/zh-Hans/images/features-action.png differ diff --git a/docs/zh-Hans/images/features-modal.png b/docs/zh-Hans/images/features-modal.png new file mode 100644 index 0000000000..74ee8aabc3 Binary files /dev/null and b/docs/zh-Hans/images/features-modal.png differ diff --git a/docs/zh-Hans/images/issue-first-test.png b/docs/zh-Hans/images/issue-first-test.png new file mode 100644 index 0000000000..b2be1aa8eb Binary files /dev/null and b/docs/zh-Hans/images/issue-first-test.png differ diff --git a/docs/zh-Hans/images/issue-list.png b/docs/zh-Hans/images/issue-list.png new file mode 100644 index 0000000000..96e9010ed6 Binary files /dev/null and b/docs/zh-Hans/images/issue-list.png differ diff --git a/docs/zh-Hans/images/js-message-confirm.png b/docs/zh-Hans/images/js-message-confirm.png new file mode 100644 index 0000000000..876ae8d98e Binary files /dev/null and b/docs/zh-Hans/images/js-message-confirm.png differ diff --git a/docs/zh-Hans/images/js-message-error.png b/docs/zh-Hans/images/js-message-error.png new file mode 100644 index 0000000000..fd8abb1d7e Binary files /dev/null and b/docs/zh-Hans/images/js-message-error.png differ diff --git a/docs/zh-Hans/images/js-message-success.png b/docs/zh-Hans/images/js-message-success.png new file mode 100644 index 0000000000..bbf418a37c Binary files /dev/null and b/docs/zh-Hans/images/js-message-success.png differ diff --git a/docs/zh-Hans/images/js-notify-success.png b/docs/zh-Hans/images/js-notify-success.png new file mode 100644 index 0000000000..04489bc7ce Binary files /dev/null and b/docs/zh-Hans/images/js-notify-success.png differ diff --git a/docs/zh-Hans/images/lepton-theme-account-layout.png b/docs/zh-Hans/images/lepton-theme-account-layout.png new file mode 100644 index 0000000000..e627ba3eae Binary files /dev/null and b/docs/zh-Hans/images/lepton-theme-account-layout.png differ diff --git a/docs/zh-Hans/images/lepton-theme-application-layout.png b/docs/zh-Hans/images/lepton-theme-application-layout.png new file mode 100644 index 0000000000..3d89929abc Binary files /dev/null and b/docs/zh-Hans/images/lepton-theme-application-layout.png differ diff --git a/docs/zh-Hans/images/lepton-theme-blazor-layout.png b/docs/zh-Hans/images/lepton-theme-blazor-layout.png new file mode 100644 index 0000000000..e4211b367e Binary files /dev/null and b/docs/zh-Hans/images/lepton-theme-blazor-layout.png differ diff --git a/docs/zh-Hans/images/modal-example-product-create.png b/docs/zh-Hans/images/modal-example-product-create.png new file mode 100644 index 0000000000..89ea0226ef Binary files /dev/null and b/docs/zh-Hans/images/modal-example-product-create.png differ diff --git a/docs/zh-Hans/images/modal-example-product-info.png b/docs/zh-Hans/images/modal-example-product-info.png new file mode 100644 index 0000000000..5d037c1125 Binary files /dev/null and b/docs/zh-Hans/images/modal-example-product-info.png differ diff --git a/docs/zh-Hans/images/modal-manager-cancel-warning.png b/docs/zh-Hans/images/modal-manager-cancel-warning.png new file mode 100644 index 0000000000..fc00902e0f Binary files /dev/null and b/docs/zh-Hans/images/modal-manager-cancel-warning.png differ diff --git a/docs/zh-Hans/images/modal-manager-example-modal.png b/docs/zh-Hans/images/modal-manager-example-modal.png new file mode 100644 index 0000000000..867c6d410e Binary files /dev/null and b/docs/zh-Hans/images/modal-manager-example-modal.png differ diff --git a/docs/zh-Hans/images/modal-manager-validation.png b/docs/zh-Hans/images/modal-manager-validation.png new file mode 100644 index 0000000000..516a178e66 Binary files /dev/null and b/docs/zh-Hans/images/modal-manager-validation.png differ diff --git a/docs/zh-Hans/images/modal-page-on-rider.png b/docs/zh-Hans/images/modal-page-on-rider.png new file mode 100644 index 0000000000..522885489d Binary files /dev/null and b/docs/zh-Hans/images/modal-page-on-rider.png differ diff --git a/docs/zh-Hans/images/nav-main-menu-administration.png b/docs/zh-Hans/images/nav-main-menu-administration.png new file mode 100644 index 0000000000..21ad977264 Binary files /dev/null and b/docs/zh-Hans/images/nav-main-menu-administration.png differ diff --git a/docs/zh-Hans/images/nav-main-menu.png b/docs/zh-Hans/images/nav-main-menu.png new file mode 100644 index 0000000000..2784c035c2 Binary files /dev/null and b/docs/zh-Hans/images/nav-main-menu.png differ diff --git a/docs/zh-Hans/images/page-alert-account-layout.png b/docs/zh-Hans/images/page-alert-account-layout.png new file mode 100644 index 0000000000..2442cb5d14 Binary files /dev/null and b/docs/zh-Hans/images/page-alert-account-layout.png differ diff --git a/docs/zh-Hans/images/page-alert-example.png b/docs/zh-Hans/images/page-alert-example.png new file mode 100644 index 0000000000..266671757a Binary files /dev/null and b/docs/zh-Hans/images/page-alert-example.png differ diff --git a/docs/zh-Hans/images/product-create-modal-page-on-rider.png b/docs/zh-Hans/images/product-create-modal-page-on-rider.png new file mode 100644 index 0000000000..01cf93eaf0 Binary files /dev/null and b/docs/zh-Hans/images/product-create-modal-page-on-rider.png differ diff --git a/docs/zh-Hans/images/react-native-introduction.gif b/docs/zh-Hans/images/react-native-introduction.gif new file mode 100644 index 0000000000..15963556aa Binary files /dev/null and b/docs/zh-Hans/images/react-native-introduction.gif differ diff --git a/docs/zh-Hans/images/solution-test-projects.png b/docs/zh-Hans/images/solution-test-projects.png new file mode 100644 index 0000000000..e51c368e9f Binary files /dev/null and b/docs/zh-Hans/images/solution-test-projects.png differ diff --git a/docs/zh-Hans/images/ui-busy.png b/docs/zh-Hans/images/ui-busy.png new file mode 100644 index 0000000000..63a1d7d74a Binary files /dev/null and b/docs/zh-Hans/images/ui-busy.png differ diff --git a/docs/zh-Hans/images/ui-options.png b/docs/zh-Hans/images/ui-options.png new file mode 100644 index 0000000000..bd211a8914 Binary files /dev/null and b/docs/zh-Hans/images/ui-options.png differ diff --git a/docs/zh-Hans/images/user-menu.png b/docs/zh-Hans/images/user-menu.png new file mode 100644 index 0000000000..af0671dbdc Binary files /dev/null and b/docs/zh-Hans/images/user-menu.png differ diff --git a/docs/zh-Hans/images/vs-run-tests-in-parallel.png b/docs/zh-Hans/images/vs-run-tests-in-parallel.png new file mode 100644 index 0000000000..765cba5100 Binary files /dev/null and b/docs/zh-Hans/images/vs-run-tests-in-parallel.png differ diff --git a/docs/zh-Hans/images/vs-startup-template-tests.png b/docs/zh-Hans/images/vs-startup-template-tests.png new file mode 100644 index 0000000000..f16d79a4c7 Binary files /dev/null and b/docs/zh-Hans/images/vs-startup-template-tests.png differ diff --git a/docs/zh-Hans/images/vs-test-explorer.png b/docs/zh-Hans/images/vs-test-explorer.png new file mode 100644 index 0000000000..e225ee5e92 Binary files /dev/null and b/docs/zh-Hans/images/vs-test-explorer.png differ diff --git a/framework/.editorconfig b/framework/.editorconfig deleted file mode 100644 index 9f20b90112..0000000000 --- a/framework/.editorconfig +++ /dev/null @@ -1,131 +0,0 @@ -# Rules in this file were initially inferred by Visual Studio IntelliCode from the D:\Projects\Volosoft\abp\framework codebase based on best match to current usage at 2.10.2020. -# You can modify the rules from these initially generated values to suit your own policies -# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference -[*.cs] - - -#Core editorconfig formatting - indentation - -#use soft tabs (spaces) for indentation -indent_style = space - -#Formatting - indentation options - -#indent switch case contents. -csharp_indent_case_contents = true -#indent switch labels -csharp_indent_switch_labels = true - -#Formatting - new line options - -#place catch statements on a new line -csharp_new_line_before_catch = true -#place else statements on a new line -csharp_new_line_before_else = true -#require members of object intializers to be on separate lines -csharp_new_line_before_members_in_object_initializers = true -#require braces to be on a new line for accessors, methods, lambdas, object_collection_array_initializers, control_blocks, types, and properties (also known as "Allman" style) -csharp_new_line_before_open_brace = accessors, methods, lambdas, object_collection_array_initializers, control_blocks, types, properties - -#Formatting - organize using options - -#sort System.* using directives alphabetically, and place them before other usings -dotnet_sort_system_directives_first = true - -#Formatting - spacing options - -csharp_space_after_cast = false -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_after_comma = true -csharp_space_after_dot = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_before_comma = false -csharp_space_before_dot = false -csharp_space_before_open_square_brackets = false -csharp_space_before_semicolon_in_for_statement = false -csharp_space_between_empty_square_brackets = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_name_and_open_parenthesis = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false -csharp_space_between_square_brackets = false - -#Formatting - wrapping options - -#leave code block on single line -csharp_preserve_single_line_blocks = true - -#Style - Code block preferences - -#prefer curly braces even for one line of code -csharp_prefer_braces = true:suggestion - -#Style - expression bodied member options - -#prefer block bodies for constructors -csharp_style_expression_bodied_constructors = false:suggestion -#prefer block bodies for methods -csharp_style_expression_bodied_methods = false:suggestion -#prefer expression-bodied members for properties -csharp_style_expression_bodied_properties = true:suggestion - -#Style - expression level options - -#prefer out variables to be declared inline in the argument list of a method call when possible -csharp_style_inlined_variable_declaration = true:suggestion -#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them -dotnet_style_predefined_type_for_member_access = true:suggestion - -#Style - Expression-level preferences - -#prefer default over default(T) -csharp_prefer_simple_default_expression = true:suggestion -#prefer objects to be initialized using object initializers when possible -dotnet_style_object_initializer = true:suggestion -#prefer inferred tuple element names -dotnet_style_prefer_inferred_tuple_names = true:suggestion - -#Style - implicit and explicit types - -#prefer var over explicit type in all cases, unless overridden by another code style rule -csharp_style_var_elsewhere = true:suggestion -#prefer var is used to declare variables with built-in system types such as int -csharp_style_var_for_built_in_types = true:suggestion -#prefer var when the type is already mentioned on the right-hand side of a declaration expression -csharp_style_var_when_type_is_apparent = true:suggestion - -#Style - language keyword and framework type options - -#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion - -#Style - modifier options - -#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. -dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion - -#Style - Modifier preferences - -#when this rule is set to a list of modifiers, prefer the specified ordering. -csharp_preferred_modifier_order = public,protected,private,virtual,async,static,override,readonly,abstract:suggestion - -#Style - Pattern matching - -#prefer pattern matching instead of is expression with type casts -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion - -#Style - qualification options - -#prefer fields not to be prefaced with this. or Me. in Visual Basic -dotnet_style_qualification_for_field = false:suggestion -#prefer methods not to be prefaced with this. or Me. in Visual Basic -dotnet_style_qualification_for_method = false:suggestion -#prefer properties not to be prefaced with this. or Me. in Visual Basic -dotnet_style_qualification_for_property = false:suggestion diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 257c039265..d76d818644 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -359,6 +359,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Compone EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Swashbuckle", "src\Volo.Abp.Swashbuckle\Volo.Abp.Swashbuckle.csproj", "{DD9519E0-5A68-48DC-A051-7BF2AC922F3E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Json.Tests", "test\Volo.Abp.Json.Tests\Volo.Abp.Json.Tests.csproj", "{00D07595-993C-40FC-BD90-0DD6331414D3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Http.Tests", "test\Volo.Abp.Http.Tests\Volo.Abp.Http.Tests.csproj", "{A37BFEB5-7C57-4CDC-93B8-B5CE4BB9ACE1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1061,14 +1065,14 @@ Global {DD9519E0-5A68-48DC-A051-7BF2AC922F3E}.Debug|Any CPU.Build.0 = Debug|Any CPU {DD9519E0-5A68-48DC-A051-7BF2AC922F3E}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD9519E0-5A68-48DC-A051-7BF2AC922F3E}.Release|Any CPU.Build.0 = Release|Any CPU - {C9142DED-1F6C-4385-A37D-81E46B233306}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C9142DED-1F6C-4385-A37D-81E46B233306}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C9142DED-1F6C-4385-A37D-81E46B233306}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C9142DED-1F6C-4385-A37D-81E46B233306}.Release|Any CPU.Build.0 = Release|Any CPU - {A30D63B0-E952-4052-BAEE-38B8BF924093}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A30D63B0-E952-4052-BAEE-38B8BF924093}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A30D63B0-E952-4052-BAEE-38B8BF924093}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A30D63B0-E952-4052-BAEE-38B8BF924093}.Release|Any CPU.Build.0 = Release|Any CPU + {00D07595-993C-40FC-BD90-0DD6331414D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00D07595-993C-40FC-BD90-0DD6331414D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00D07595-993C-40FC-BD90-0DD6331414D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00D07595-993C-40FC-BD90-0DD6331414D3}.Release|Any CPU.Build.0 = Release|Any CPU + {A37BFEB5-7C57-4CDC-93B8-B5CE4BB9ACE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A37BFEB5-7C57-4CDC-93B8-B5CE4BB9ACE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A37BFEB5-7C57-4CDC-93B8-B5CE4BB9ACE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A37BFEB5-7C57-4CDC-93B8-B5CE4BB9ACE1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1248,8 +1252,8 @@ Global {B9D1ADCB-D552-4626-A1F1-78FF72C1E822} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {89840441-5A3A-4FD7-9CB4-E5B52FAEF72A} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {DD9519E0-5A68-48DC-A051-7BF2AC922F3E} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} - {C9142DED-1F6C-4385-A37D-81E46B233306} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} - {A30D63B0-E952-4052-BAEE-38B8BF924093} = {447C8A77-E5F0-4538-8687-7383196D04EA} + {00D07595-993C-40FC-BD90-0DD6331414D3} = {447C8A77-E5F0-4538-8687-7383196D04EA} + {A37BFEB5-7C57-4CDC-93B8-B5CE4BB9ACE1} = {447C8A77-E5F0-4538-8687-7383196D04EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj b/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj index 97b9c6dc0a..64f2ef9f99 100644 --- a/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj @@ -19,7 +19,7 @@ - + diff --git a/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Microsoft/Extensions/DependencyInjection/AbpOpenIdConnectExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Microsoft/Extensions/DependencyInjection/AbpOpenIdConnectExtensions.cs index 56886f679e..e98280d19d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Microsoft/Extensions/DependencyInjection/AbpOpenIdConnectExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Microsoft/Extensions/DependencyInjection/AbpOpenIdConnectExtensions.cs @@ -25,26 +25,29 @@ namespace Microsoft.Extensions.DependencyInjection { options.ClaimActions.MapAbpClaimTypes(); - options.Events = new OpenIdConnectEvents + configureOptions?.Invoke(options); + + options.Events ??= new OpenIdConnectEvents(); + var authorizationCodeReceived = options.Events.OnAuthorizationCodeReceived ?? (_ => Task.CompletedTask); + + options.Events.OnAuthorizationCodeReceived = receivedContext => { - OnAuthorizationCodeReceived = receivedContext => - { - var tenantKey = receivedContext.HttpContext.RequestServices - .GetRequiredService>().Value.TenantKey; - - if (receivedContext.HttpContext.Request != null && - receivedContext.Request.Cookies.ContainsKey(tenantKey)) - { - receivedContext.TokenEndpointRequest.SetParameter(tenantKey, - receivedContext.Request.Cookies[tenantKey]); - } - - return Task.CompletedTask; - } + SetAbpTenantId(receivedContext); + return authorizationCodeReceived.Invoke(receivedContext); }; - - configureOptions?.Invoke(options); }); } + + private static void SetAbpTenantId(AuthorizationCodeReceivedContext receivedContext) + { + var tenantKey = receivedContext.HttpContext.RequestServices + .GetRequiredService>().Value.TenantKey; + + if (receivedContext.Request.Cookies.ContainsKey(tenantKey)) + { + receivedContext.TokenEndpointRequest.SetParameter(tenantKey, + receivedContext.Request.Cookies[tenantKey]); + } + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Volo.Abp.AspNetCore.Authentication.OpenIdConnect.csproj b/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Volo.Abp.AspNetCore.Authentication.OpenIdConnect.csproj index bac9441f00..b0aa11f206 100644 --- a/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Volo.Abp.AspNetCore.Authentication.OpenIdConnect.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Volo.Abp.AspNetCore.Authentication.OpenIdConnect.csproj @@ -9,7 +9,7 @@ - + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeBundleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeBundleContributor.cs new file mode 100644 index 0000000000..edef97eb52 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeBundleContributor.cs @@ -0,0 +1,17 @@ +using Volo.Abp.Bundling; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme +{ + public class BasicThemeBundleContributor : IBundleContributor + { + public void AddScripts(BundleContext context) + { + + } + + public void AddStyles(BundleContext context) + { + context.Add("_content/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/libs/abp/css/theme.css"); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/Branding.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/Branding.razor new file mode 100644 index 0000000000..5e0537b2b2 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/Branding.razor @@ -0,0 +1,9 @@ +@using Volo.Abp.Ui.Branding +@inject IBrandingProvider BrandingProvider + + @if (!BrandingProvider.LogoUrl.IsNullOrWhiteSpace()) + { + @BrandingProvider.AppName + } + @BrandingProvider.AppName + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor index 574a684213..b2fab0807d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor @@ -3,19 +3,19 @@ var elementId = MenuItem.ElementId ?? "MenuItem_" + MenuItem.Name.Replace(".", "_"); var cssClass = string.IsNullOrEmpty(MenuItem.CssClass) ? string.Empty : MenuItem.CssClass; var disabled = MenuItem.IsDisabled ? "disabled" : string.Empty; - var url = string.IsNullOrEmpty(MenuItem.Url) ? "#" : MenuItem.Url; + var url = MenuItem.Url == null ? "#" : MenuItem.Url.TrimStart('/'); } @if (MenuItem.IsLeaf) { if (MenuItem.Url != null) {