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..b9576fda4c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.user *.userosscache *.sln.docstates +*.editorconfig # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000000..dea0a6c6f7 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,29 @@ + + + + + 5.0.0 + + + 16.6.1 + + + 4.2.2 + + + 3.0.2 + + + 2.4.1 + + + 2.4.1 + + + 2.4.2 + + + 2.2.14 + + + \ No newline at end of file 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/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/es.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/es.json index dbd028baae..89ef8706e0 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/es.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/es.json @@ -116,7 +116,7 @@ "UsernameOrEmailPlaceholder": "Usuario o email...", "Member": "Miembro", "PurchaseOrderNo": "Número de orden de compra", - "QuotationDate": "", + "QuotationDate": "Fecha de presupuesto", "CompanyName": "Nombre de empresa", "CompanyAddress": "Dirección de empresa", "Price": "Precio", 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/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.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index 2a57c6b25d..e029499b06 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -30,6 +30,8 @@ "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." } } \ 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.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json index 87db8c931a..c570e6fb1e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json @@ -85,6 +85,20 @@ "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 Articles", + "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." } } 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..6f9f0726cf --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/de-DE.json @@ -0,0 +1,189 @@ +{ + "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." + } +} \ No newline at end of file diff --git a/common.DotSettings b/common.DotSettings index 6f40d029a7..7edbd05e75 100644 --- a/common.DotSettings +++ b/common.DotSettings @@ -20,6 +20,17 @@ False False SQL + False + Never + Never + False + Never + Never + Never + Never + Never + True + True False False False diff --git a/common.props b/common.props index 9f6b677e72..1528abbcc6 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ latest - 4.0.0 + 4.1.0 $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ diff --git a/docs/en/API/Dynamic-CSharp-API-Clients.md b/docs/en/API/Dynamic-CSharp-API-Clients.md index 23ec336acf..d92846f5e8 100644 --- a/docs/en/API/Dynamic-CSharp-API-Clients.md +++ b/docs/en/API/Dynamic-CSharp-API-Clients.md @@ -1,4 +1,4 @@ -# 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. 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/Authorization.md b/docs/en/Authorization.md index eef4237039..158466d153 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 @@ -354,7 +354,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/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..2e1b9d2a90 100644 --- a/docs/en/Background-Workers.md +++ b/docs/en/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/en/Blob-Storing.md b/docs/en/Blob-Storing.md index f477e6178a..63281a5ee6 100644 --- a/docs/en/Blob-Storing.md +++ b/docs/en/Blob-Storing.md @@ -21,7 +21,7 @@ The ABP Framework has already the following storage provider implementations; * [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/CLI.md b/docs/en/CLI.md index 8713bc9019..a119770286 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 @@ -159,6 +160,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-template` 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 +170,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 @@ -383,10 +394,27 @@ 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. + +For more details about managing style and script references in Blazor apps, see [Managing Global Scripts & Styles](UI/Blazor/Global-Scripts-Styles.md) \ No newline at end of file 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..1342407880 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; @@ -93,4 +93,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-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-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-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/Customizing-Application-Modules-Overriding-Services.md b/docs/en/Customizing-Application-Modules-Overriding-Services.md index b8706eea61..133b2e89eb 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..."); @@ -214,7 +213,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 +285,8 @@ ObjectExtensionManager.Instance .AddOrUpdateProperty( new[] { - typeof(IdentityUserDto), - typeof(IdentityUserCreateDto), + typeof(IdentityUserDto), + typeof(IdentityUserCreateDto), typeof(IdentityUserUpdateDto) }, "SocialSecurityNumber" 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..b0a9692f83 --- /dev/null +++ b/docs/en/Domain-Driven-Design-Implementation-Guide.md @@ -0,0 +1,95 @@ +# Implementing Domain Driven Design + +## Introduction + +### Goals + +This is an **integrative guide** for implementing the Domain Driven Design (DDD). The goals of this document are; + +* **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. + +### 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 your 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 **react 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 contains 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**: 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 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 + +TODO + +### Execution Flow a DDD Based Application + +TODO + +### Common Principles + +TODO \ No newline at end of file diff --git a/docs/en/Domain-Driven-Design.md b/docs/en/Domain-Driven-Design.md index ce33295492..f3d2699cab 100644 --- a/docs/en/Domain-Driven-Design.md +++ b/docs/en/Domain-Driven-Design.md @@ -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. +DDD mostly interest in the **Domain** and the **Application** layers, rather than the Infrastructure and the Presentation layers. + ## Contents +See the following documents to learn what ABP Framework provides to you to implement DDD in your project. + * **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) \ 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/Entity-Framework-Core.md b/docs/en/Entity-Framework-Core.md index 98163e7c28..8c0a1c9cc9 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,22 @@ 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); + }); +}); +```` + ## 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..04cc6fc695 100644 --- a/docs/en/Exception-Handling.md +++ b/docs/en/Exception-Handling.md @@ -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.md b/docs/en/Getting-Started.md index 1e473b266f..6bfacc9a53 100644 --- a/docs/en/Getting-Started.md +++ b/docs/en/Getting-Started.md @@ -22,15 +22,14 @@ First things first! Let's setup your development environment before creating the 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/) - +* [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) @@ -202,6 +201,8 @@ Right click to the `.DbMigrator` project and select **Set as StartUp Project** {{ 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. @@ -238,6 +239,8 @@ Ensure that the `.Web` project is the startup project. Run the application which {{ 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. 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..d381ffceb6 --- /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 MyProjectNameBundleContributer : IBundleContributer + { + 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..8bc4924000 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. -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; +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. -* Set `UseV3UrlStyle` to `true` in the options of the `options.ConventionalControllers.Create(...)` method. Example: +## Moved to 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 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,105 @@ 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 the the `ExtraPropertyDictionary`. + +## 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). \ No newline at end of file diff --git a/docs/en/MongoDB.md b/docs/en/MongoDB.md index 475eb94f1b..98769c13b8 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,4 @@ 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. diff --git a/docs/en/Repositories.md b/docs/en/Repositories.md index 0fc80984e7..5ff8372af8 100644 --- a/docs/en/Repositories.md +++ b/docs/en/Repositories.md @@ -79,6 +79,14 @@ If your entity does not have an Id primary key (it may have a composite primary > `IRepository` has a few missing methods those normally works with the `Id` property of an entity. Because of the entity has no `Id` property in that case, these methods are not available. One example is the `Get` method that gets an id and returns the entity with given id. However, you can still use `IQueryable` features to query entities by standard LINQ 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 really delete the entity from database in case of you need it. + +See the [Data Filtering](Data-Filtering.md) documentation for more about soft-delete. + ## Custom Repositories Default generic repositories will be sufficient for most cases. However, you may need to create a custom repository class for your entity. diff --git a/docs/en/Road-Map.md b/docs/en/Road-Map.md index 8670ba080b..3a45ca1767 100644 --- a/docs/en/Road-Map.md +++ b/docs/en/Road-Map.md @@ -1,17 +1,12 @@ # 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; * [#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/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/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..b080087ba8 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 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"]
@@ -1408,27 +1433,19 @@ Here the complete code to create the book management CRUD page, that has been de ShowPager="true" PageSize="PageSize"> - + - - - @L["Actions"] - - - - @L["Edit"] - - - @L["Delete"] - - - + + + + - + @@ -1436,7 +1453,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..8dab28a8db 100644 --- a/docs/en/Tutorials/Part-4.md +++ b/docs/en/Tutorials/Part-4.md @@ -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-9.md b/docs/en/Tutorials/Part-9.md index 374efff2b4..65a59f39e1 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; } 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/Component-Replacement.md b/docs/en/UI/Angular/Component-Replacement.md index 577129e139..6df847d6d0 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 @@ -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/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/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..38e7969eea --- /dev/null +++ b/docs/en/UI/Angular/Form-Validation.md @@ -0,0 +1,140 @@ +# 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 providing the `VALIDATION_BLUEPRINTS` injection token from your root module. + +```js +import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; + +@NgModule({ + // rest of the module metadata + + providers: [ + // other providers + { + provide: VALIDATION_BLUEPRINTS, + useValue: { + uniqueUsername: "::AlreadyExists[{%{{{ username }}}%}]", + }, + }, + ], +}) +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 providing `VALIDATION_BLUEPRINTS` injection token from 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"; + +@NgModule({ + // rest of the module metadata + + providers: [ + // other providers + { + provide: VALIDATION_BLUEPRINTS, + useValue: { + required: "::RequiredInput", + }, + }, + ], +}) +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..7cc31a46fd 100644 --- a/docs/en/UI/Angular/List-Service.md +++ b/docs/en/UI/Angular/List-Service.md @@ -177,8 +177,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..e7e284bb0d 100644 --- a/docs/en/UI/Angular/Multi-Tenancy.md +++ b/docs/en/UI/Angular/Multi-Tenancy.md @@ -126,7 +126,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/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..f24ae62982 --- /dev/null +++ b/docs/en/UI/Angular/Testing.md @@ -0,0 +1,3 @@ +# Angular UI: Testing + +TODO \ No newline at end of file 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/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/AspNetCore/Branding.md b/docs/en/UI/AspNetCore/Branding.md index b262c13d46..e4a9ee0f3b 100644 --- a/docs/en/UI/AspNetCore/Branding.md +++ b/docs/en/UI/AspNetCore/Branding.md @@ -1,3 +1,45 @@ # ASP.NET Core MVC / Razor Pages: Branding -TODO \ No newline at end of file +## 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"; + } +} +```` + +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. + +## Overriding the Branding Area + +The [Basic Theme](Basic-Theme.md) doesn't implement the logos. However, 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. + +An example screenshot with an image is used in the branding area: + +![bookstore-added-logo](../../images/bookstore-added-logo.png) \ No newline at end of file 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 d2a1193a53..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](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. +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/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/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 13a6e1678c..762d6a4b0b 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) @@ -89,7 +101,7 @@ There are more options of a menu item (the constructor of the `ApplicationMenuIt * `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. 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/Tag-Helpers/Index.md b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md index 54c37af688..f85edb5142 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Index.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Index.md @@ -13,7 +13,7 @@ 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) @@ -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/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/Toolbars.md b/docs/en/UI/AspNetCore/Toolbars.md index 524ef3f18b..3f1185f61b 100644 --- a/docs/en/UI/AspNetCore/Toolbars.md +++ b/docs/en/UI/AspNetCore/Toolbars.md @@ -1,4 +1,4 @@ -# Toolbars +# ASP.NET Core MVC / Razor Pages 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**. diff --git a/docs/en/UI/Blazor/Authentication.md b/docs/en/UI/Blazor/Authentication.md new file mode 100644 index 0000000000..6c1cdb31f5 --- /dev/null +++ b/docs/en/UI/Blazor/Authentication.md @@ -0,0 +1,3 @@ +# Blazor UI: Authentication + +TODO \ No newline at end of file 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/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/Global-Scripts-Styles.md b/docs/en/UI/Blazor/Global-Scripts-Styles.md new file mode 100644 index 0000000000..a9a69c76d0 --- /dev/null +++ b/docs/en/UI/Blazor/Global-Scripts-Styles.md @@ -0,0 +1,38 @@ +# 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. + +> Bundle command adds style and script references individually. Bundling and minification support will be added to incoming releases. \ No newline at end of file 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..331de7a4cb --- /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 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..cf8287f0de 100644 --- a/docs/en/UI/Blazor/Overall.md +++ b/docs/en/UI/Blazor/Overall.md @@ -1,6 +1,155 @@ -# 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. + +## 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/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/Theming.md b/docs/en/UI/Blazor/Theming.md new file mode 100644 index 0000000000..a3ecca784b --- /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.Mvc.UI.Theme.Basic` 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 `IBundleContributer` 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 MyThemeBundleContributer : IBundleContributer + { + 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..884b0bbcd0 100644 --- a/docs/en/Upgrading.md +++ b/docs/en/Upgrading.md @@ -16,6 +16,17 @@ 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. +### 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 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. 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/docs-nav.json b/docs/en/docs-nav.json index ab1e840376..3a174bb7b1 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -344,14 +344,16 @@ "path": "Entities.md" }, { - "text": "Value Objects" + "text": "Value Objects", + "path": "Value-Objects.md" }, { "text": "Repositories", "path": "Repositories.md" }, { - "text": "Domain Services" + "text": "Domain Services", + "path": "Domain-Services.md" }, { "text": "Specifications", @@ -404,8 +406,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" @@ -426,6 +432,10 @@ "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" @@ -460,10 +470,18 @@ "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", @@ -549,13 +567,58 @@ "path": "UI/Blazor/Overall.md" }, { - "text": "Services", + "text": "Navigation / Menu", + "path": "UI/Blazor/Navigation-Menu.md" + }, + { + "text": "Localization", + "path": "UI/Blazor/Localization.md" + }, + { + "text": "Settings", + "path": "UI/Blazor/Settings.md" + }, + { + "text": "Notification", + "path": "UI/Blazor/Notification.md" + }, + { + "text": "Message", + "path": "UI/Blazor/Message.md" + }, + { + "text": "Theming", + "path": "UI/Blazor/Theming.md", "items": [ { - "text": "Overall", - "path": "UI/Blazor/Services/Notification.md" + "text": "The Basic Theme", + "path": "UI/Blazor/Basic-Theme.md" } ] + }, + { + "text": "Toolbars", + "path": "UI/Blazor/Toolbars.md" + }, + { + "text": "Page Alerts", + "path": "UI/Blazor/Page-Alerts.md" + }, + { + "text": "Branding", + "path": "UI/Blazor/Branding.md" + }, + { + "text": "Customization / Overriding Components", + "path": "UI/Blazor/Customization-Overriding-Components.md" + }, + { + "text": "Global Scripts & Styles", + "path": "UI/Blazor/Global-Scripts-Styles.md" + }, + { + "text": "Authentication", + "path": "UI/Blazor/Authentication.md" } ] }, @@ -595,8 +658,8 @@ "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": "HTTP Requests", @@ -606,6 +669,10 @@ "text": "Localization", "path": "UI/Angular/Localization.md" }, + { + "text": "Form Validation", + "path": "UI/Angular/Form-Validation.md" + }, { "text": "Settings", "path": "UI/Angular/Settings.md" @@ -809,6 +876,10 @@ } ] }, + { + "text": "Testing", + "path": "Testing.md" + }, { "text": "Startup Templates", "items": [ 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/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/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/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-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/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/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/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/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.md b/docs/zh-Hans/Blob-Storing.md index 99688ca47a..c233f3359e 100644 --- a/docs/zh-Hans/Blob-Storing.md +++ b/docs/zh-Hans/Blob-Storing.md @@ -21,7 +21,7 @@ ABP框架已经有以下存储提供程序的实现; * [Azure](Blob-Storing-Azure.md): 将BLOG存储在 [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/)中. +* [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框架. @@ -306,4 +306,4 @@ Configure(options => ## 另请参阅 -* [创建自定义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..2dfe827ffc 100644 --- a/docs/zh-Hans/CLI.md +++ b/docs/zh-Hans/CLI.md @@ -125,7 +125,7 @@ abp update [options] * `--nuget`: 仅更新的NuGet包 * `--solution-path` 或 `-sp`: 指定解决方案路径/目录. 默认使用当前目录 * `--solution-name` 或 `-sn`: 指定解决方案名称. 默认在目录中搜索`*.sln`文件. -*`--check-all`: 分别检查每个包的新版本. 默认是 `false`. +* `--check-all`: 分别检查每个包的新版本. 默认是 `false`. ### 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-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/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/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/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/Tutorials/Part-1.md b/docs/zh-Hans/Tutorials/Part-1.md index 99a7e3d9fb..e459c8b960 100644 --- a/docs/zh-Hans/Tutorials/Part-1.md +++ b/docs/zh-Hans/Tutorials/Part-1.md @@ -84,7 +84,7 @@ namespace Acme.BookStore.Books } ```` -* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是**[领域驱动设计](./Domain-Driven-Design.md)** 概念之一. 可以视为直接查询和处理的根实体(请参阅[实体文档](./Entities.md)). +* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是[**领域驱动设计**](./Domain-Driven-Design.md) 概念之一. 可以视为直接查询和处理的根实体(请参阅[实体文档](./Entities.md)). * `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些审计属性(`CreationTime`, `CreatorId`, `LastModificationTime` 等). ABP框架自动为你管理这些属性. * `Guid`是`Book`实体的主键类型. @@ -185,7 +185,7 @@ namespace Acme.BookStore.EntityFrameworkCore ### 添加数据迁移 -启动模板使用[EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)创建和维护数据库架构. 打开菜单*工具 > 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/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/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 65cac6e7cd..963472ada8 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -355,6 +355,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 @@ -1057,6 +1061,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 + {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 @@ -1236,6 +1248,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} + {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/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..4810320664 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/Branding.razor @@ -0,0 +1,3 @@ +@using Volo.Abp.Ui.Branding +@inject IBrandingProvider BrandingProvider +@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..31090ac092 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 @@ -15,7 +15,7 @@ { if (MenuItem.Icon.StartsWith("fa")) { - + } } @MenuItem.DisplayName @@ -32,7 +32,7 @@ else { if (MenuItem.Icon.StartsWith("fa")) { - + } } @MenuItem.DisplayName diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LanguageSwitch.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LanguageSwitch.razor index 8c7b348f7d..6b93618f44 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LanguageSwitch.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LanguageSwitch.razor @@ -5,23 +5,23 @@ @inject IJSRuntime JsRuntime @if (_otherLanguages != null && _otherLanguages.Any()) { - - + + @_currentLanguage.DisplayName - - + + @foreach (var language in _otherLanguages) { - @language.DisplayName + @language.DisplayName } - - + + } @code { private IReadOnlyList _otherLanguages; private LanguageInfo _currentLanguage; - protected override async Task OnInitializedAsync() + protected async override Task OnInitializedAsync() { var selectedLanguageName = await JsRuntime.InvokeAsync( "localStorage.getItem", diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor index e4767cc031..58d4d5753e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor @@ -26,9 +26,6 @@ @menuItem.DisplayName } } - - @UiLocalizer["ManageYourAccount"] - Logout diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs index 17718dbcfb..494eca34ab 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs @@ -1,50 +1,33 @@ using System; using System.Threading.Tasks; -using Localization.Resources.AbpUi; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Routing; -using Microsoft.Extensions.Localization; -using Microsoft.Extensions.Options; -using Volo.Abp.Http.Client; using Volo.Abp.UI.Navigation; namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic { public partial class LoginDisplay : IDisposable { - [Inject] - protected IMenuManager MenuManager { get; set; } - - [Inject] - protected IStringLocalizer UiLocalizer { get; set; } - [Inject] - protected IOptions RemoteServiceOptions { get; set; } - - protected ApplicationMenu Menu { get; set; } + protected IMenuManager MenuManager { get; set; } - protected string ServerUrl { get; set; } - protected string ServerAccountUrl { get; set; } + protected ApplicationMenu Menu { get; set; } - protected override async Task OnInitializedAsync() + protected async override Task OnInitializedAsync() { Menu = await MenuManager.GetAsync(StandardMenus.User); - - ServerUrl = RemoteServiceOptions.Value.RemoteServices.Default?.BaseUrl?.TrimEnd('/'); - ServerAccountUrl = ServerUrl + "/Account/Manage?returnUrl=" + Navigation.Uri; - + Navigation.LocationChanged += OnLocationChanged; } protected virtual void OnLocationChanged(object sender, LocationChangedEventArgs e) { - ServerAccountUrl = ServerUrl + "/Account/Manage?returnUrl=" + Navigation.Uri; StateHasChanged(); } - + public void Dispose() { Navigation.LocationChanged -= OnLocationChanged; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/MainLayout.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/MainLayout.razor index 45496a7984..e85e899123 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/MainLayout.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/MainLayout.razor @@ -1,9 +1,7 @@ @inherits LayoutComponentBase -@using Volo.Abp.Ui.Branding -@inject IBrandingProvider BrandingProvider