@ -0,0 +1,29 @@ |
|||
<Project> |
|||
<PropertyGroup> |
|||
|
|||
<!-- All Microsoft packages --> |
|||
<MicrosoftPackageVersion>5.0.0</MicrosoftPackageVersion> |
|||
|
|||
<!-- Microsoft.NET.Test.Sdk https://www.nuget.org/packages/Microsoft.NET.Test.Sdk --> |
|||
<MicrosoftNETTestSdkPackageVersion>16.6.1</MicrosoftNETTestSdkPackageVersion> |
|||
|
|||
<!-- NSubstitute https://www.nuget.org/packages/NSubstitute --> |
|||
<NSubstitutePackageVersion>4.2.2</NSubstitutePackageVersion> |
|||
|
|||
<!-- Shouldly https://www.nuget.org/packages/Shouldly --> |
|||
<ShouldlyPackageVersion>3.0.2</ShouldlyPackageVersion> |
|||
|
|||
<!-- xunit https://www.nuget.org/packages/xUnit --> |
|||
<xUnitPackageVersion>2.4.1</xUnitPackageVersion> |
|||
|
|||
<!-- xunit.extensibility.execution https://www.nuget.org/packages/xunit.extensibility.execution --> |
|||
<xUnitExtensibilityExecutionPackageVersion>2.4.1</xUnitExtensibilityExecutionPackageVersion> |
|||
|
|||
<!-- xunit.runner.visualstudio https://www.nuget.org/packages/xunit.runner.visualstudio --> |
|||
<xUnitRunnerVisualstudioPackageVersion>2.4.2</xUnitRunnerVisualstudioPackageVersion> |
|||
|
|||
<!-- Mongo2Go https://www.nuget.org/packages/Mongo2Go --> |
|||
<Mongo2GoPackageVersion>2.2.14</Mongo2GoPackageVersion> |
|||
|
|||
</PropertyGroup> |
|||
</Project> |
|||
@ -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" |
|||
} |
|||
} |
|||
@ -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" |
|||
} |
|||
} |
|||
@ -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" |
|||
} |
|||
} |
|||
@ -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." |
|||
} |
|||
} |
|||
@ -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." |
|||
} |
|||
} |
|||
@ -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. <a href=\"{0}\"> Hangfire </a> & <a href=\"{1}\"> RabbitMQ </a> -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": "<a href=\"{0}\"> Object to Object Mapping </a> 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 <a href=\"{1}\"> {0} -Dokumentation </a>", |
|||
"IndexPageHeroSection": "<span class=\"first-line shine\"><strong>Open Source</strong></span><span class=\"second-line text-uppercase\">Webanwendung<br />Framework </span><span class=\"third-line shine2\"><strong>für ASP.Net Core</strong></span>", |
|||
"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 <a href=\"{0}\">JavaScript</a> & <a href=\"{1}\">C#</a>.", |
|||
"EmailSMSAbstractionsDocument": "Weitere Informationen finden Sie in den Unterlagen <a href=\"{0}\">E-Mail-Senden</a> and <a href=\"{1}\">SMS-Senden</a>.", |
|||
"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": "<span class=\"text-primary\">Lesen Sie</span><span class=\"text-success\">Die Dokumentation</span>", |
|||
"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 <strong>new</strong> erstellt eine <strong>mehrschichtige MVC-Anwendung</strong> mit <strong>Entity Framework Core</strong> als Datenbankanbieter. Es gibt jedoch zusätzliche Optionen. Beispiele:", |
|||
"SeeCliDocumentForMoreInformation": "Weitere Optionen finden Sie im <a href=\"{0}\">ABP CLI-Dokument</a> oder wählen Sie oben die Registerkarte \"Direkter Download\".", |
|||
"Optional": "Optional", |
|||
"LocalFrameworkRef": "Behalten Sie die lokale Projektreferenz für die Framework-Pakete bei." |
|||
} |
|||
} |
|||
@ -1,3 +1,133 @@ |
|||
# RabbitMQ Background Job Manager |
|||
|
|||
TODO |
|||
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<AbpRabbitMqOptions>(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<AbpRabbitMqBackgroundJobOptions>(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) |
|||
@ -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** |
|||
|
|||
 |
|||
|
|||
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**. |
|||
|
After Width: | Height: | Size: 28 KiB |
@ -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. |
|||
|
|||
 |
|||
|
|||
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<AbpLocalizationOptions>`. 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")); |
|||
``` |
|||
|
|||
 |
|||
|
|||
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. |
|||
 |
|||
|
|||
|
|||
|
|||
Close the IIS Express / Kestrel to invalidate the language cache and run the project. You will see the new language on your website. |
|||
|
|||
 |
|||
|
|||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 26 KiB |
@ -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. |
|||
> — <cite>Johan Cruyff</cite> |
|||
|
|||
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. |
|||
> — <cite>???</cite> |
|||
|
|||
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; |
|||
|
|||
 |
|||
|
|||
**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**: |
|||
|
|||
 |
|||
|
|||
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 |
|||
@ -1,3 +1,135 @@ |
|||
# ABP Documentation |
|||
# Domain Services |
|||
|
|||
TODO! |
|||
## 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<Issue, Guid> _issueRepository; |
|||
|
|||
public IssueManager(IRepository<Issue, Guid> 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<Guid> |
|||
{ |
|||
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<AppUser, Guid> _userRepository; |
|||
private readonly IRepository<Issue, Guid> _issueRepository; |
|||
|
|||
public IssueAppService( |
|||
IssueManager issueManager, |
|||
IRepository<AppUser, Guid> userRepository, |
|||
IRepository<Issue, Guid> 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. |
|||
@ -1,3 +1 @@ |
|||
# Integration Tests |
|||
|
|||
TODO! |
|||
This document has been [moved to here](Testing.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. |
|||
@ -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 `<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>` 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 `<link...>` elements and replace with the following comment tags: |
|||
|
|||
````html |
|||
<!--ABP:Styles--> |
|||
<!--/ABP:Styles--> |
|||
```` |
|||
|
|||
3. Remove all the `<script...>` elements and replace with the following comment tags: |
|||
|
|||
````html |
|||
<!--ABP:Scripts--> |
|||
<!--/ABP:Scripts--> |
|||
```` |
|||
|
|||
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 `<app>...</app>` to `<div id="ApplicationContainer">...</div>` in the `wwwroot/index.html`. |
|||
* Change `builder.RootComponents.Add<App>("app");` to `builder.RootComponents.Add<App>("#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)). |
|||
@ -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. |
|||
|
|||
@ -1,3 +1,255 @@ |
|||
# Specifications |
|||
|
|||
TODO! |
|||
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<Guid> |
|||
{ |
|||
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<Customer>`. |
|||
|
|||
**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<Customer> |
|||
{ |
|||
public override Expression<Func<Customer, bool>> 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<T>` interface, but the `Specification<T>` 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<Customer, Guid> _customerRepository; |
|||
|
|||
public CustomerManager(IRepository<Customer, Guid> customerRepository) |
|||
{ |
|||
_customerRepository = customerRepository; |
|||
} |
|||
|
|||
public async Task<List<Customer>> 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<Customer> |
|||
{ |
|||
public override Expression<Func<Customer, bool>> 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<Customer, Guid> _customerRepository; |
|||
|
|||
public CustomerManager(IRepository<Customer, Guid> customerRepository) |
|||
{ |
|||
_customerRepository = customerRepository; |
|||
} |
|||
|
|||
public async Task<int> 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<Customer> |
|||
{ |
|||
public AdultPremiumCustomerSpecification() |
|||
: base(new Age18PlusCustomerSpecification(), |
|||
new PremiumCustomerSpecification()) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Now, you can re-write the `GetAdultPremiumCustomerCountAsync` method as shown below: |
|||
|
|||
````csharp |
|||
public async Task<int> 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. |
|||
@ -1,3 +1,673 @@ |
|||
# Testing |
|||
# Automated Testing |
|||
|
|||
TODO! |
|||
## 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: |
|||
|
|||
 |
|||
|
|||
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: |
|||
|
|||
 |
|||
|
|||
### 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: |
|||
|
|||
 |
|||
|
|||
### 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*. |
|||
|
|||
 |
|||
|
|||
## 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<Guid> |
|||
{ |
|||
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: |
|||
|
|||
 |
|||
|
|||
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<IssueStateException>(() => |
|||
{ |
|||
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<IIssueRepository>(); |
|||
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<IIssueRepository>` 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<IIssueRepository>(); |
|||
fakeRepo |
|||
.GetIssueCountOfUserAsync(userId) |
|||
.Returns(IssueManager.MaxAllowedOpenIssueCountForAUser); |
|||
|
|||
var issueManager = new IssueManager(fakeRepo); |
|||
|
|||
// Act & Assert |
|||
|
|||
var issue = new Issue(); |
|||
|
|||
await Assert.ThrowsAsync<BusinessException>(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<IIssueRepository>(); |
|||
_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<BusinessException>(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<T>` 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<IssueManager>(); |
|||
_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<BusinessException>(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<List<IssueDto>> GetListAsync() |
|||
{ |
|||
var issues = await _issueRepository.GetListAsync(); |
|||
|
|||
return ObjectMapper.Map<List<Issue>, List<IssueDto>>(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<IIssueAppService>(); |
|||
} |
|||
|
|||
[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. |
|||
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 55 KiB |
@ -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) |
|||
@ -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. |
|||
@ -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: |
|||
|
|||
<img alt="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." src="./images/form-validation---error-display-user-experience.gif" width="990px" style="max-width:100%"> |
|||
|
|||
## 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: |
|||
|
|||
<img alt="An already taken username is entered while creating new user and a custom error message appears under the input after validation." src="./images/form-validation---new-error-message.gif" width="990px" style="max-width:100%"> |
|||
|
|||
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: |
|||
|
|||
<img alt="A required field is cleared and the custom error message appears under the input." src="./images/form-validation---overwrite-error-message.gif" width="990px" style="max-width:100%"> |
|||
|
|||
## 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 |
|||
<form [formGroup]="form" skipValidation> |
|||
<!-- form fields here --> |
|||
</form> |
|||
``` |
|||
|
|||
## 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 |
|||
<input type="text" formControlName="name" skipValidation /> |
|||
``` |
|||
|
|||
## 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: ` |
|||
<div |
|||
class="font-weight-bold font-italic px-1 invalid-feedback" |
|||
*ngFor="let error of abpErrors; trackBy: trackByFn" |
|||
> |
|||
{%{{{ error.message | abpLocalization: error.interpoliteParams }}}%} |
|||
</div> |
|||
`, |
|||
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: |
|||
|
|||
<img alt="A required field is cleared and a bold and italic error message appears." src="./images/form-validation---custom-error-template.gif" width="990px" style="max-width:100%"> |
|||
@ -0,0 +1,3 @@ |
|||
# Angular UI: Testing |
|||
|
|||
TODO |
|||
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 906 KiB |
|
After Width: | Height: | Size: 466 KiB |
|
After Width: | Height: | Size: 488 KiB |
@ -1,3 +1,45 @@ |
|||
# ASP.NET Core MVC / Razor Pages: Branding |
|||
|
|||
TODO |
|||
## 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: |
|||
|
|||
 |
|||
|
|||
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: |
|||
|
|||
 |
|||
|
|||
`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: |
|||
|
|||
 |
|||
@ -1,3 +1,91 @@ |
|||
# Dynamic JavaScript HTTP API Proxies |
|||
# Dynamic JavaScript API Client Proxies |
|||
|
|||
TODO |
|||
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<AuthorDto> GetAsync(Guid id); |
|||
|
|||
Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input); |
|||
|
|||
Task<AuthorDto> CreateAsync(CreateAuthorDto input); |
|||
|
|||
Task UpdateAsync(Guid id, UpdateAuthorDto input); |
|||
|
|||
Task DeleteAsync(Guid id); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> 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) |
|||