Merge branch 'rel-4.2' of https://github.com/abpframework/abp into auto-merge/rel-4-1/157

pull/7650/head
mehmet-erim 5 years ago
commit 6e21421fae

@ -1,24 +1,24 @@
name: Merge branch rel-4.2 with rel-4.1
name: Merge branch dev with rel-4.2
on:
push:
branches:
- rel-4.1
- rel-4.2
jobs:
merge-rel-4-2-with-rel-4-1:
merge-dev-with-rel-4-2:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: rel-4.2
ref: dev
- name: Reset promotion branch
run: |
git fetch origin rel-4.1:rel-4.1
git reset --hard rel-4.1
git fetch origin rel-4.2:rel-4.2
git reset --hard rel-4.2
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with:
branch: auto-merge/rel-4-1/${{github.run_number}}
title: Merge branch rel-4.2 with rel-4.1
body: This PR generated automatically to merge rel-4.2 with rel-4.1. Please review the changed files before merging to prevent any errors that may occur.
branch: auto-merge/rel-4-2/${{github.run_number}}
title: Merge branch dev with rel-4.2
body: This PR generated automatically to merge dev with rel-4.2. Please review the changed files before merging to prevent any errors that may occur.
reviewers: ${{github.actor}}
token: ${{ github.token }}

@ -2,7 +2,8 @@
![build and test](https://github.com/abpframework/abp/workflows/build%20and%20test/badge.svg)
[![NuGet](https://img.shields.io/nuget/v/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core)
[![MyGet (with prereleases)](https://img.shields.io/myget/abp-nightly/vpre/Volo.Abp.svg?style=flat-square)](https://docs.abp.io/en/abp/latest/Nightly-Builds)
[![NuGet (with prereleases)](https://img.shields.io/nuget/vpre/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core)
[![MyGet (nightly builds)](https://img.shields.io/myget/abp-nightly/vpre/Volo.Abp.svg?style=flat-square)](https://docs.abp.io/en/abp/latest/Nightly-Builds)
[![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core)
ABP Framework is a complete **infrastructure** based on the **ASP.NET Core** to create **modern web applications** and **APIs** by following the software development **best practices** and the **latest technologies**.
@ -103,4 +104,4 @@ ABP is a community-driven open source project. See [the contribution guide](http
## Support the ABP Framework
Love ABP Framework? **Please give a star** to this repository :star:
Love ABP Framework? **Please give a star** to this repository :star:

@ -0,0 +1,14 @@
{
"culture": "en-GB",
"texts": {
"Account": "ABP Account - Login & Register | ABP.IO",
"Welcome": "Welcome",
"UseOneOfTheFollowingLinksToContinue": "Use one of the following links to continue",
"FrameworkHomePage": "Framework home page",
"FrameworkDocumentation": "Framework documentation",
"OfficialBlog": "Official blog",
"CommercialHomePage": "Commercial home page",
"CommercialSupportWebSite": "Commercial support web site",
"CommunityWebSite": "ABP community web site"
}
}

@ -0,0 +1,202 @@
{
"culture": "en-GB",
"texts": {
"Permission:Organizations": "Organisations",
"Permission:Manage": "Manage Organisations",
"Permission:DiscountRequests": "Discount Requests",
"Permission:DiscountManage": "Manage Discount Requests",
"Permission:Disable": "Disable",
"Permission:Enable": "Enable",
"Permission:EnableSendEmail": "Enable Send Email",
"Permission:SendEmail": "Send Email",
"Permission:NpmPackages": "NPM Packages",
"Permission:NugetPackages": "Nuget Packages",
"Permission:Maintenance": "Maintenance",
"Permission:Maintain": "Maintain",
"Permission:ClearCaches": "Clear caches",
"Permission:Modules": "Modules",
"Permission:Packages": "Packages",
"Permission:Edit": "Edit",
"Permission:Delete": "Delete",
"Permission:Create": "Create",
"Permission:Accounting": "Accounting",
"Permission:Accounting:Quotation": "Quotation",
"Permission:Accounting:Invoice": "Invoice",
"Menu:Organizations": "Organisations",
"Menu:Accounting": "Accounting",
"Menu:Packages": "Packages",
"Menu:DiscountRequests": "Discount Requests",
"NpmPackageDeletionWarningMessage": "This NPM Package will be deleted. Please confirm this?",
"NugetPackageDeletionWarningMessage": "This Nuget Package will be deleted. Please confirm this?",
"ModuleDeletionWarningMessage": "This Module will be deleted. Please confirm this?",
"Name": "Name",
"DisplayName": "Display name",
"ShortDescription": "Short description",
"NameFilter": "Name",
"CreationTime": "Creation time",
"IsPro": "Is pro",
"ShowOnModuleList": "Show in module list",
"EfCoreConfigureMethodName": "Configure method name",
"IsProFilter": "Is pro",
"ApplicationType": "Application type",
"Target": "Target",
"TargetFilter": "Target",
"ModuleClass": "Module class",
"NugetPackageTarget.DomainShared": "Domain Shared",
"NugetPackageTarget.Domain": "Domain",
"NugetPackageTarget.Application": "Application",
"NugetPackageTarget.ApplicationContracts": "Application Contracts",
"NugetPackageTarget.HttpApi": "Http Api",
"NugetPackageTarget.HttpApiClient": "Http Api Client",
"NugetPackageTarget.Web": "Web",
"NugetPackageTarget.EntityFrameworkCore": "Delete Entity Framework Core",
"NugetPackageTarget.MongoDB": "MongoDB",
"Edit": "Edit",
"Delete": "Delete",
"Refresh": "Refresh",
"NpmPackages": "NPM Packages",
"NugetPackages": "Nuget Packages",
"NpmPackageCount": "NPM Package Count",
"NugetPackageCount": "Nuget Package Count",
"Module": "Modules",
"ModuleInfo": "Module info",
"CreateANpmPackage": "Create a NPM package",
"CreateAModule": "Create a module",
"CreateANugetPackage": "Create a Nuget package",
"AddNew": "Add new",
"PackageAlreadyExist{0}": "\"{0}\" package has already been added.",
"ModuleAlreadyExist{0}": "\"{0}\" module has already been added.",
"ClearCache": "Clear cache",
"SuccessfullyCleared": "Successfully cleared",
"Menu:NpmPackages": "NPM Packages",
"Menu:Modules": "Modules",
"Menu:Maintenance": "Maintenance",
"Menu:NugetPackages": "Nuget Packages",
"CreateAnOrganization": "Create an organisation",
"Organizations": "Organisations",
"LongName": "Long name",
"LicenseType": "License type",
"MissingLicenseTypeField": "The license type field is required!",
"LicenseStartTime": "License start time",
"LicenseEndTime": "License end time",
"AllowedDeveloperCount": "Allowed developer count",
"UserNameOrEmailAddress": "Username or email address",
"AddOwner": "Add owner",
"UserName": "Username",
"Email": "Email",
"Developers": "Developers",
"AddDeveloper": "Add developer",
"Create": "Create",
"UserNotFound": "User not found",
"{0}WillBeRemovedFromDevelopers": "{0} Will be removed from developers, please confirm?",
"{0}WillBeRemovedFromOwners": "{0} Will be removed from owners, please confirm?",
"Computers": "Computers",
"UniqueComputerId": "Unique computer id",
"LastSeenDate": "Last seen date",
"{0}Computer{1}WillBeRemovedFromRecords": "Computer of {0} ({1}) will be removed from records",
"OrganizationDeletionWarningMessage": "Organisation will be deleted",
"DeletingLastOwnerWarningMessage": "An organisation must have at least one owner, therefore you cannot remove this owner",
"This{0}AlreadyExistInThisOrganization": "{0} already exist in this organisation",
"AreYouSureYouWantToDeleteAllComputers": "Are you sure you want to delete all computers?",
"DeleteAll": "Delete all",
"DoYouWantToCreateNewUser": "Do you want to create new user?",
"MasterModules": "Master Modules",
"OrganizationName": "Organisation name",
"CreationDate": "Creation date",
"LicenseStartDate": "License start date",
"LicenseEndDate": "License end date",
"OrganizationNamePlaceholder": "Organisation name...",
"TotalQuestionCountPlaceholder": "Total question count...",
"RemainingQuestionCountPlaceholder": "Remaining question count...",
"LicenseTypePlaceholder": "License type...",
"CreationDatePlaceholder": "Creation date...",
"LicenseStartDatePlaceholder": "License start date...",
"LicenseEndDatePlaceholder": "License end date...",
"UsernameOrEmail": "Username or email",
"UsernameOrEmailPlaceholder": "Username or email...",
"Member": "Member",
"PurchaseOrderNo": "Purchase Order No.",
"QuotationDate": "Quotation date",
"CompanyName": "Company name",
"CompanyAddress": "Company address",
"Price": "Price",
"DiscountText": "Discount text",
"DiscountQuantity": "Discount quantity",
"DiscountPrice": "Discount price",
"Quotation": "Quotation",
"ExtraText": "Extra text",
"ExtraAmount": "Extra Amount",
"DownloadQuotation": "Download Quotation",
"Invoice": "Invoice",
"TaxNumber": "Tax No.",
"InvoiceNumber": "Invoice No.",
"InvoiceDate": "Invoice Date",
"InvoiceNote": "Invoice Note",
"Quantity": "Quantity",
"AddProduct": "Add Product",
"AddProductWarning": "You need to add a Product!",
"TotalPrice": "Total Price",
"Generate": "Generate",
"MissingQuantityField": "The Quantity field is required!",
"MissingPriceField": "The Price field is required!",
"CodeUsageStatus": "Status",
"Country": "Country",
"DeveloperCount": "Developer Count",
"RequestCode": "Request Code",
"WebSite": "Website",
"GithubUsername": "Github Username",
"PhoneNumber": "Phone Number",
"ProjectDescription": "Project Description",
"Referrer": "Referrer",
"DiscountRequests": "Discount Request",
"Copylink": "Copy Link",
"Disable": "Disable",
"Enable": "Enable",
"EnableSendEmail": "Enable Send Email",
"SendEmail": "Send Email",
"SuccessfullyDisabled": "Successfully disabled",
"SuccessfullyEnabled": "Successfully enabled",
"EmailSent": "Email sent",
"SuccessfullySent": "Successfully sent",
"SuccessfullyDeleted": "Successfully deleted",
"DiscountRequestDeletionWarningMessage": "Discount request will be deleted",
"BusinessType": "Business Type",
"TotalQuestionCount": "Total question count",
"RemainingQuestionCount": "Remaining question count",
"TotalQuestionMustBeGreaterWarningMessage": "TotalQuestionCount must be greater than RemainingQuestionCount!",
"QuestionCountsMustBeGreaterThanZero": "TotalQuestionCount and RemainingQuestionCount must be zero or greater than zero!",
"UnlimitedQuestionCount": "Unlimited question count",
"Notes": "Notes",
"Menu:Community": "Community",
"Menu:Articles": "Articles",
"Wait": "Wait",
"Approve": "Approve",
"Reject": "Reject",
"Details": "Details",
"Url": "Url",
"Title": "Title",
"ContentSource": "Content source",
"Status": "Status",
"ReadArticle": "Read article",
"ArticleHasBeenWaiting": "Article has been waiting",
"ArticleHasBeenApproved": "Article has been approved",
"ArticleHasBeenRejected": "Article has been rejected",
"Permission:Community": "Community",
"Permission:CommunityArticle": "Article",
"Link": "Link",
"Enum:ContentSource:0": "Github",
"Enum:ContentSource:1": "External",
"Enum:Status:0": "Waiting",
"Enum:Status:1": "Author name",
"Enum:Status:2": "Approved",
"Summary": "Summary",
"AuthorName": "Author",
"CoverImage": "Cover Image",
"RemoveCacheConfirmationMessage": "Are you sure you want remove the cache for \"{0}\" article?",
"SuccessfullyRemoved": "Successfully cleared",
"RemoveCache": "Remove Cache",
"Language": "Language",
"Optional": "Optional",
"CreateArticleLanguageInfo": "The language in which the article is written"
}
}

@ -197,6 +197,7 @@
"RemoveCache": "Remove Cache",
"Language": "Language",
"Optional": "Optional",
"CreateArticleLanguageInfo": "The language in which the article is written"
"CreateArticleLanguageInfo": "The language in which the post is written",
"Enum:ContentSource:2": "Video Post"
}
}
}

@ -0,0 +1,35 @@
{
"culture": "en-GB",
"texts": {
"Volo.AbpIo.Domain:010004": "Maximum member count reached!",
"Volo.AbpIo.Domain:010005": "Maximum owner count reached!",
"Volo.AbpIo.Domain:010006": "This user is already an owner in this organisation!",
"Volo.AbpIo.Domain:010007": "This user is already a developer in this organisation!",
"Volo.AbpIo.Domain:010008": "Allowed Developer Count can not be less then current developer count!",
"Volo.AbpIo.Domain:010009": "Allowed Developer Count can not be less then 0!",
"Volo.AbpIo.Domain:010010": "Maximum mac address count has been exceeded!",
"Volo.AbpIo.Domain:010011": "Personal license can't have more than 1 developer!",
"Volo.AbpIo.Domain:010012": "License can't be extended one month after license expires!",
"Volo.AbpIo.Domain:020001": "Couldn't delete this NPM Package because \"{NugetPackages}\" Nuget Packages are dependent to this package.",
"Volo.AbpIo.Domain:020002": "Couldn't delete this NPM Package because \"{Modules}\" Modules are using this package.",
"Volo.AbpIo.Domain:020003": "Couldn't delete this NPM Package because \"{Modules}\" Modules are using this package and \"{NugetPackages}\" Nuget Packages are dependent to this package.",
"Volo.AbpIo.Domain:020004": "Couldn't delete this Nuget Package because \"{Modules}\" Modules are using this package.",
"WantToLearn?": "Want to learn?",
"ReadyToGetStarted?": "Ready to get started?",
"JoinOurCommunity": "Join our community",
"GetStartedUpper": "GET STARTED",
"ForkMeOnGitHub": "Fork me on GitHub",
"Features": "Features",
"GetStarted": "Get Started",
"Documents": "Documents",
"Community": "Community",
"ContributionGuide": "Contribution Guide",
"Blog": "Blog",
"Commercial": "Commercial",
"MyAccount": "My account",
"Permission:License": "License",
"Permission:UserInfo": "User info",
"SeeDocuments": "See Documents",
"Samples": "Samples"
}
}

@ -27,6 +27,8 @@
"Blog": "博客",
"Commercial": "商业版",
"MyAccount": "我的账户",
"Permission:License": "许可",
"Permission:UserInfo": "用户信息",
"SeeDocuments": "查看文档",
"Samples": "示例"
}

@ -0,0 +1,39 @@
{
"culture": "en-GB",
"texts": {
"OrganizationManagement": "Organisation Management",
"OrganizationList": "Organisation list",
"Volo.AbpIo.Commercial:010003": "You are not owner of this organisation!",
"OrganizationNotFoundMessage": "Organisation not found!",
"DeveloperCount": "Allocated / total developers",
"QuestionCount": "Remaining / total questions",
"Unlimited": "Unlimited",
"Owners": "Owners",
"AddMember": "Add member",
"AddOwner": "Add owner",
"AddDeveloper": "Add developer",
"UserName": "Username",
"Name": "Name",
"EmailAddress": "Email address",
"Developers": "Developers",
"LicenseType": "License type",
"Manage": "Manage",
"StartDate": "Start date",
"EndDate": "End date",
"Modules": "Modules",
"LicenseExtendMessage": "Your license end date is extended to {0}",
"LicenseUpgradeMessage": "Your license is upgraded to {0}",
"LicenseAddDeveloperMessage": "{0} developers added to your license",
"Volo.AbpIo.Commercial:010004": "Cannot find the specified user! The user must have already registered.",
"MyOrganizations": "My organisations",
"ApiKey": "API key",
"UserNameNotFound": "There is no user with username {0}",
"SuccessfullyAddedToNewsletter": "Thank you for subscribing to our newsletter!",
"MyProfile": "My Profile",
"EmailNotValid": "Please enter a valid email address.",
"JoinOurMarketingNewsletter": "Join our marketing newsletter",
"WouldLikeToReceiveMarketingMaterials": "I would like to receive marketing materials e.g. product deals & special offers.",
"StartUsingYourLicenseNow": "Start using your license now!",
"WelcomePage": "Welcome Page"
}
}

@ -28,8 +28,12 @@
"MyOrganizations": "我的组织",
"ApiKey": "API key",
"UserNameNotFound": "没有用户名为{0}的用户",
"SuccessfullyAddedToNewsletter": "感谢你订阅我们的新闻讯!",
"SuccessfullyAddedToNewsletter": "感谢你订阅我们的新闻讯!",
"MyProfile": "我的资料",
"EmailNotValid": "请输入有效的电子邮件地址"
"EmailNotValid": "请输入有效的电子邮件地址",
"JoinOurMarketingNewsletter": "加入我们的营销简讯",
"WouldLikeToReceiveMarketingMaterials": "我想收到市场营销资料,例如产品交易和特别优惠.",
"StartUsingYourLicenseNow": "立即开始使用你的许可证",
"WelcomePage": "欢迎页面"
}
}

@ -0,0 +1,106 @@
{
"culture": "en-GB",
"texts": {
"Permission:CommunityArticle": "Community Article",
"Permission:Edit": "Edit",
"Waiting": "Waiting",
"Approved": "Approved",
"Rejected": "Rejected",
"Wait": "Wait",
"Approve": "Approve",
"Reject": "Reject",
"ReadArticle": "Read Article",
"Status": "Status",
"ContentSource": "Content Source",
"Details": "Details",
"Url": "Url",
"Title": "Title",
"CreationTime": "Creation time",
"Save": "Save",
"SameUrlAlreadyExist": "The Url already exists. If you want to add this article, you should change the url!",
"UrlIsNotValid": "Url is not valid.",
"UrlNotFound": "Url not found.",
"UrlContentNotFound": "Url content not found.",
"Summary": "Summary",
"MostRead": "Most Read",
"Latest": "Latest",
"ContributeAbpCommunity": "Contribute to the ABP Community",
"SubmitYourArticle": "Submit Your Article",
"ContributionGuide": "Contribution Guide",
"BugReport": "Bug Report",
"SeeAllArticles": "See All Articles",
"WelcomeToABPCommunity!": "Welcome to the ABP Community!",
"MyProfile": "My Profile",
"MyOrganizations": "My Organisations",
"EmailNotValid": "Please enter a valid email address.",
"FeatureRequest": "Feature Request",
"CreateArticleTitleInfo": "Title of the article to be shown on the article list.",
"CreateArticleUrlInfo": "Original GitHub/External URL of the article.",
"CreateArticleSummaryInfo": "A short summary of the article to be shown on the article list.",
"CreateArticleCoverInfo": "To create an effective article, add a cover photo and an upload 16:9 aspect ratio pictures for the best view (Maximum file size: 1MB)",
"ThisExtensionIsNotAllowed": "The extension is not allowed.",
"TheFileIsTooLarge": "The file is too large.",
"GoToTheArticle": "Go to the Article",
"Contribute": "Contribute",
"OverallProgress": "Overall Progress",
"Done": "Done",
"Open": "Open",
"Closed": "Closed",
"LatestQuestionOnThe": "Latest Question on the",
"Stackoverflow": "Stackoverflow",
"Votes": "votes",
"Answer": "Answer",
"Views": "views",
"Answered": "Answered",
"WaitingForYourAnswer": "Waiting for your answer",
"Asked": "asked",
"AllQuestions": "All Questions",
"NextVersion": "Next Version",
"MilestoneErrorMessage": "Couldn't get the current milestone details from Github.",
"QuestionItemErrorMessage": "Couldn't get the latest question details from Stackoverflow.",
"Oops": "Oops!",
"CreateArticleSuccessMessage": "The Article has been successfully submitted. It will be published once it has been reviewed by the site admin.",
"ChooseCoverImage": "Choose a cover image...",
"CoverImage": "Cover Image",
"ShareYourExperiencesWithTheABPFramework": "Share your experiences with the ABP Framework!",
"Optional": "Optional",
"UpdateUserWebSiteInfo": "Example: https://johndoe.com",
"UpdateUserTwitterInfo": "Example: johndoe",
"UpdateUserGithubInfo": "Example: johndoe",
"UpdateUserLinkedinInfo": "Example: https://www.linkedin.com/...",
"UpdateUserCompanyInfo": "Example: Volosoft",
"UpdateUserJobTitleInfo": "Example: Software Developer",
"UserName": "UserName",
"Company": "Company",
"PersonalWebsite": "Personal website",
"RegistrationDate": "Registration date",
"Social": "Social",
"Biography": "Biography",
"HasNoPublishedArticlesYet": "has no published articles yet",
"Author": "Author",
"LatestGithubAnnouncements": "Latest GitHub Announcements",
"SeeAllAnnouncements": "View all Announcements",
"LatestBlogPost": "Latest Blog Post",
"Edit": "Edit",
"ProfileImageChange": "Change the profile image",
"BlogItemErrorMessage": "Couldn't get the latest blog post details from ABP.",
"PlannedReleaseDate": "Planned release date",
"CommunityArticleRequestErrorMessage": "Couldn't get the latest article request from GitHub.",
"ArticleRequestFromGithubIssue": "There aren't any article requests at the moment.",
"LatestArticles": "Latest Articles",
"ArticleRequests": "Article Requests",
"AllArticleRequests": "See all Article requests",
"SubscribeToTheNewsletter": "Subscribe to the Newsletter",
"NewsletterEmailDefinition": "Receive information about what's happening in ABP; new releases, free sources, articles, and more.",
"NoThanks": "No thanks",
"MaybeLater": "Maybe later",
"JoinOurArticleNewsletter": "Join our article newsletter",
"Community": "Community",
"Marketing": "Marketing",
"CommunityPrivacyPolicyConfirmation": "I agree to the Terms & Conditions and <a href=\"https://commercial.abp.io/Privacy\">Privacy Policy</a>.",
"ArticleRequestMessageTitle": "<a href=\"https://github.com/abpframework/abp/issues/new\">Open an issue</a> on the GitHub to request an article/tutorial you want to see on this web site.",
"ArticleRequestMessageBody": "Here is the list of the requested articles by the Community. Do you want to write a requested article? Please click on the request and join the discussion.",
"Language": "Language",
"CreateArticleLanguageInfo": "The language in which the article is written"
}
}

@ -25,19 +25,18 @@
"MostRead": "Most Read",
"Latest": "Latest",
"ContributeAbpCommunity": "Contribute to the ABP Community",
"SubmitYourArticle": "Submit Your Article",
"SubmitYourArticle": "Submit Your Post",
"ContributionGuide": "Contribution Guide",
"BugReport": "Bug Report",
"SeeAllArticles": "See All Articles",
"SeeAllArticles": "See All Posts",
"WelcomeToABPCommunity!": "Welcome to the ABP Community!",
"MyProfile": "My profile",
"MyOrganizations": "My organizations",
"EmailNotValid": "Please enter a valid email address.",
"FeatureRequest": "Feature Request",
"CreateArticleTitleInfo": "Title of the article to be shown on the article list.",
"CreateArticleUrlInfo": "Original GitHub/External URL of the article.",
"CreateArticleSummaryInfo": "A short summary of the article to be shown on the article list.",
"CreateArticleCoverInfo": "For creating an effective article, add a cover photo. Upload 16:9 aspect ratio pictures for the best view.",
"CreateArticleTitleInfo": "Title of the post to be shown on the post list.",
"CreateArticleSummaryInfo": "A short summary of the post to be shown on the post list.",
"CreateArticleCoverInfo": "For creating an effective article, add a cover photo. Upload 16:9 aspect ratio pictures for the best view. Maximum file size: 1MB.",
"ThisExtensionIsNotAllowed": "This extension is not allowed.",
"TheFileIsTooLarge": "The file is too large.",
"GoToTheArticle": "Go to the Article",
@ -87,7 +86,7 @@
"PlannedReleaseDate": "Planned release date",
"CommunityArticleRequestErrorMessage": "Could not get the latest article request from Github.",
"ArticleRequestFromGithubIssue": "There are not any article requests now.",
"LatestArticles": "Latest Articles",
"LatestArticles": "Latest Posts",
"ArticleRequests": "Article Requests",
"AllArticleRequests": "See All Article Requests",
"SubscribeToTheNewsletter": "Subscribe to the Newsletter",
@ -101,6 +100,38 @@
"ArticleRequestMessageTitle": "<a href=\"https://github.com/abpframework/abp/issues/new\">Open an issue</a> on the GitHub to request an article/tutorial you want to see on this web site.",
"ArticleRequestMessageBody": "Here, the list of the requested articles by the community. Do you want to write a requested article? Please click to the request and join to the discussion.",
"Language": "Language",
"CreateArticleLanguageInfo": "The language in which the article is written"
"CreateArticleLanguageInfo": "The language for the post content.",
"VideoPost": "Video Post",
"Article": "Article",
"Read": "Read",
"CreateGithubArticleUrlInfo": "Original GitHub URL of the article.",
"CreateVideoContentUrlInfo": "Original Youtube URL of the post.",
"CreateExternalArticleUrlInfo": "Original External Url of the article.",
"VideoContentForm": "Submit Video on YouTube",
"GithubPostForm": "Submit Article on GitHub",
"ExternalPostForm": "Submit an External Content",
"HowToPost": "How to Post?",
"Posts": "Posts",
"VideoUrl": "Video Url",
"GithubArticleUrl": "Github Article Url",
"ExternalArticleUrl": "External Article Url",
"CreatePostCoverInfo": "For creating an effective post, add a cover photo. Upload 16:9 aspect ratio pictures for the best view. Maximum file size: 1MB.",
"ThankYouForContribution": "Thank you for contributing to the ABP Community.",
"GithubArticle": "Github Article",
"GithubArticleSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Write an article on any public GitHub repository with the Markdown format. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">example</a>",
"GithubArticleSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Submit your article URL using the form.",
"GithubArticleSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Your article will be rendered in this web site.",
"YoutubeVideo": "Youtube Video",
"YoutubeVideoSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Publish your video on YouTube.",
"YoutubeVideoSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Submit the video URL using the form.",
"YoutubeVideoSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Visitors will be able to watch your video content directly on this website.",
"ExternalContent": "External Content",
"ExternalContentSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Create a content on any public platform (medium, your own blog or anywhere you like).",
"ExternalContentSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Submit your content URL using the form.",
"ExternalContentSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Visitors are redirected to the content on the original website.",
"ChooseYourContentType": "Please choose the way you want to add your content.",
"PostContentViaGithub": "I want to add my article with <span class=\"font-weight-bold\"><i class=\"fa fa-github\"></i> GitHub</span> in accordance with the markdown rules.",
"PostContentViaYoutube": "I want to share my videos available on <span class=\"font-weight-bold\"><i class=\"fa fa-youtube\"></i> Youtube</span> here.",
"PostContentViaExternalSource": "I want to add the content I published on <span class=\"font-weight-bold\">another platform</span> here."
}
}

@ -35,7 +35,6 @@
"EmailNotValid": "请输入有效的电子邮箱地址.",
"FeatureRequest": "功能请求",
"CreateArticleTitleInfo": "文章标题显示在文章列表中.",
"CreateArticleUrlInfo": "文章的原始GitHub/外部URL.",
"CreateArticleSummaryInfo": "文章的简短摘要将显示在文章列表中.",
"CreateArticleCoverInfo": "为了创建有效的文章,请添加封面图. 仅支持16:9的图片!",
"ThisExtensionIsNotAllowed": "不允许此扩展名.",
@ -84,6 +83,34 @@
"Edit": "修改",
"ProfileImageChange": "更改资料图片",
"BlogItemErrorMessage": "无法从ABP获取最新的博客文章详细信息.",
"PlannedReleaseDate": "计划发布日期"
"PlannedReleaseDate": "计划发布日期",
"CommunityArticleRequestErrorMessage": "无法从Github获取最新的文章请求.",
"ArticleRequestFromGithubIssue": "现在没有任何文章请求.",
"LatestArticles": "最新的帖子",
"ArticleRequests": "文章请求",
"AllArticleRequests": "查看所有文章请求",
"SubscribeToTheNewsletter": "订阅简讯",
"NewsletterEmailDefinition": "获取有关ABP发生的信息,例如新版本,免费资源,文章等.",
"NoThanks": "不用了,谢谢",
"MaybeLater": "以后再说",
"JoinOurArticleNewsletter": "加入我们的文章简讯",
"Community": "社区",
"Marketing": "营销",
"CommunityPrivacyPolicyConfirmation": "我同意条款和条件以及<a href=\"https://commercial.abp.io/Privacy\">隐私政策</a>.",
"ArticleRequestMessageTitle": "<a href=\"https://github.com/abpframework/abp/issues/new\">在GitHub上创建一个Issue</a>,以请求你要在此网站上查看的文章/教程.",
"ArticleRequestMessageBody": "在这里,是社区请求的文章列表. 您要写一篇要求的文章吗? 请单击该请求并加入讨论.",
"Language": "语言",
"CreateArticleLanguageInfo": "本文所用的语言",
"VideoPost": "视频",
"Article": "文章",
"Read": "阅读",
"CreateGithubArticleUrlInfo": "文章的原始GitHub链接.",
"CreateVideoContentUrlInfo": "文章的原始Youtube链接.",
"CreateExternalArticleUrlInfo": "本文的原始外部网址",
"VideoContentForm": "视频内容来源",
"GithubPostForm": "Github文章来源",
"ExternalPostForm": "外部文章来源",
"PostSourceTypeChooses": "我们接受文章的三种来源类型;",
"Posts": "文章"
}
}

@ -156,6 +156,7 @@
"UiFramework": "إطار عمل واجهة المستخدم",
"EmailAddress": "البريد الإلكترونى",
"Mobile": "المحمول",
"ReactNative": "React Native"
"ReactNative": "React Native",
"SelectLanguage": "اختار اللغة"
}
}

@ -184,6 +184,7 @@
"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."
"LocalFrameworkRef": "Behalten Sie die lokale Projektreferenz für die Framework-Pakete bei.",
"SelectLanguage": "Sprache auswählen"
}
}

@ -0,0 +1,198 @@
{
"culture": "en-GB",
"texts": {
"GetStarted": "Get Started - Startup Templates",
"Create": "Create",
"NewProject": "New Project",
"DirectDownload": "Direct Download",
"ProjectName": "Project name",
"ProjectType": "Project type",
"DatabaseProvider": "Database provider",
"NTier": "N-Tier",
"IncludeUserInterface": "Include user interface",
"CreateNow": "Create now",
"TheStartupProject": "The startup project",
"Tutorial": "Tutorial",
"UsingCLI": "Using CLI",
"SeeDetails": "See Details",
"AbpShortDescription": "ABP Framework is a complete infrastructure to create modern web applications by following the software development best practices and conventions.",
"SourceCodeUpper": "SOURCE CODE",
"LatestReleaseLogs": "Latest release logs",
"Infrastructure": "Infrastructure",
"Architecture": "Architecture",
"Modular": "Modular",
"DontRepeatYourself": "Dont Repeat Yourself",
"DeveloperFocused": "Developer Focused",
"FullStackApplicationInfrastructure": "Full stack application infrastructure.",
"DomainDrivenDesign": "Domain Driven Design",
"DomainDrivenDesignExplanation": "Designed and developed based on DDD patterns and principles. Provides a layered model for your application.",
"Authorization": "Authorisation",
"AuthorizationExplanation": "Advanced authorisation with user, role and fine-grained permission system. Built on the Microsoft Identity library.",
"MultiTenancy": "Multi-Tenancy",
"MultiTenancyExplanationShort": "SaaS applications made easy! Integrated multi-tenancy from database to UI.",
"CrossCuttingConcerns": "Cross Cutting Concerns",
"CrossCuttingConcernsExplanationShort": "Complete infrastructure for authorization, validation, exception handling, caching, audit logging, transaction management and more.",
"BuiltInBundlingMinification": "Built-In Bundling & Minification",
"BuiltInBundlingMinificationExplanation": "No need to use external tools for bundling & minification. ABP offers a simpler, dynamic, powerful, modular and built-in way!",
"VirtualFileSystem": "Virtual File System",
"VirtualFileSystemExplanation": "Embed views, scripts, styles, images... into packages/libraries and reuse them in different applications.",
"Theming": "Theming",
"ThemingExplanationShort": "Use and customise the bootstrap-based standard UI theme or create your own.",
"BootstrapTagHelpersDynamicForms": "Bootstrap Tag Helpers & Dynamic Forms",
"BootstrapTagHelpersDynamicFormsExplanation": "Instead of manually writing bootstrap components, Use ABP's tag helpers to simplify and take advantage of intellisense. Quickly build UI forms based on a C# model using the dynamic form tag helper.",
"HTTPAPIsDynamicProxies": "HTTP APIs & Dynamic Proxies",
"HTTPAPIsDynamicProxiesExplanation": "Automatically expose application services as REST style HTTP APIs, and consume them with dynamic JavaScript and C# proxies.",
"CompleteArchitectureInfo": "Modern architecture to create maintainable software solutions.",
"DomainDrivenDesignBasedLayeringModelExplanation": "Helps you to implement a DDD based layered architecture and build a maintainable code base.",
"DomainDrivenDesignBasedLayeringModelExplanationCont": "Provides startup templates, abstractions, base classes, services, documentation and guides to help you to develop your application based on DDD patterns & principles.",
"MicroserviceCompatibleModelExplanation": "The core framework & pre-build modules are designed with microservice architecture in mind.",
"MicroserviceCompatibleModelExplanationCont": "Provides infrastructure, integrations, samples and documentation to implement microservice solutions easier, while it doesnt bring additional complexity if you want a monolithic application.",
"ModularInfo": "ABP provides a module system that allows you to develop reusable application modules, tie into application lifecycle events, and express dependencies between core parts of your system.",
"PreBuiltModulesThemes": "Pre-Built Modules & Themes",
"PreBuiltModulesThemesExplanation": "Open source and commercial modules & themes are ready to use in your business application.",
"NuGetNPMPackages": "NuGet & NPM Packages",
"NuGetNPMPackagesExplanation": "Distributed as NuGet & NPM packages. Easy to install and upgrade.",
"ExtensibleReplaceable": "Extensible/Replaceable",
"ExtensibleReplaceableExplanation": "All services & modules are designed extensibility in mind. You can replace services, pages, styles and components.",
"CrossCuttingConcernsExplanation2": "Keep your codebase smaller so you can maintain focus on the code thats specific to your business.",
"CrossCuttingConcernsExplanation3": "Dont spend time implementing common application requirements on multiple projects.",
"AuthenticationAuthorization": "Authentication & Authorization",
"ExceptionHandling": "Exception Handling",
"Validation": "Validation",
"DatabaseConnection": "Database Connection",
"TransactionManagement": "Transaction management",
"AuditLogging": "Audit Logging",
"Caching": "Caching",
"Multitenancy": "Multitenancy",
"DataFiltering": "Data filtering",
"ConventionOverConfiguration": "Convention Over Configuration",
"ConventionOverConfigurationExplanation": "ABP implements common application conventions by default with a minimal or zero configuration.",
"ConventionOverConfigurationExplanationList1": "Auto registers known services to dependency injection.",
"ConventionOverConfigurationExplanationList2": "Exposes application services as HTTP APIs by naming conventions.",
"ConventionOverConfigurationExplanationList3": "Creates dynamic HTTP client proxies for C# and JavaScript.",
"ConventionOverConfigurationExplanationList4": "Provides default repositories for your entities.",
"ConventionOverConfigurationExplanationList5": "Manages Unit of Work per web request or application service method.",
"ConventionOverConfigurationExplanationList6": "Publishes create, update & delete events for your entities.",
"BaseClasses": "Base Classes",
"BaseClassesExplanation": "Pre-built base classes for common application patterns.",
"DeveloperFocusedExplanation": "ABP is for developers.",
"DeveloperFocusedExplanationCont": "It aims to simplify your daily software development while not restricting you from writing low level code.",
"SeeAllFeatures": "See All Features",
"CLI_CommandLineInterface": "CLI (Command Line Interface)",
"CLI_CommandLineInterfaceExplanation": "Includes a CLI to help you automate the creation of new projects and the addition of new modules.",
"StartupTemplates": "Startup Templates",
"StartupTemplatesExplanation": "Various startup templates provide a fully configured solution to jump start your development.",
"BasedOnFamiliarTools": "Based on Familiar Tools",
"BasedOnFamiliarToolsExplanation": "Built and integrated with popular tools you already know. Low learning curve, easy adaptation, comfortable development.",
"ORMIndependent": "ORM Independent",
"ORMIndependentExplanation": "The core framework is ORM/database independent and can work with any data source. Entity Framework Core and MongoDB providers are already available.",
"Features": "Explore the ABP Framework Features",
"ABPCLI": "ABP CLI",
"Modularity": "Modularity",
"BootstrapTagHelpers": "Bootstrap Tag Helpers",
"DynamicForms": "Dynamic Forms",
"BundlingMinification": "Bundling & Minification",
"BackgroundJobs": "Background Jobs",
"BackgroundJobsExplanation": "Define simple classes to execute jobs in the background as queued. Use the built-in job manager or integrate your own. <a href=\"{0}\">Hangfire</a> & <a href=\"{1}\">RabbitMQ</a> integrations are already available.",
"DDDInfrastructure": "DDD Infrastructure",
"DomainDrivenDesignInfrastructure": "Domain Driven Design Infrastructure",
"AutoRESTAPIs": "Auto REST APIs",
"DynamicClientProxies": "Dynamic Client Proxies",
"DistributedEventBus": "Distributed Event Bus",
"DistributedEventBusWithRabbitMQIntegration": "Distributed Event Bus with RabbitMQ Integration",
"TestInfrastructure": "Test Infrastructure",
"AuditLoggingEntityHistories": "Audit Logging & Entity Histories",
"ObjectToObjectMapping": "Object to Object Mapping",
"ObjectToObjectMappingExplanation": "<a href=\"{0}\">Object to object mapping</a> abstraction with AutoMapper integration.",
"EmailSMSAbstractions": "Email & SMS Abstractions",
"EmailSMSAbstractionsWithTemplatingSupport": "Email & SMS Abstractions with Templating Support",
"Localization": "Localization",
"SettingManagement": "Setting Management",
"ExtensionMethods": "Extension Methods",
"ExtensionMethodsHelpers": "Extension Methods & Helpers",
"AspectOrientedProgramming": "Aspect Oriented Programming",
"DependencyInjection": "Dependency Injection",
"DependencyInjectionByConventions": "Dependency Injection by Conventions",
"ABPCLIExplanation": "The ABP CLI (Command Line Interface) is a command line tool to perform common operations for ABP based solutions.",
"ModularityExplanation": "ABP provides a complete infrastructure to build your own application modules those may have entities, services, database integration, APIs, UI components and so on.",
"MultiTenancyExplanation": "ABP framework not only supports to develop multi-tenant applications, but also makes your code mostly unaware of the multi-tenancy.",
"MultiTenancyExplanation2": "Can automatically determine the current Tenant and isolate each Tenant's data from each other.",
"MultiTenancyExplanation3": "Supports single database, database per tenant and hybrid approaches.",
"MultiTenancyExplanation4": "You focus on your business code and let the framework to handle multi-tenancy on behalf of you.",
"BootstrapTagHelpersExplanation": "Instead of manually writing the repeating details of bootstrap components, use ABP's tag helpers to simplify and take advantage of intellisense. You can use Bootstrap whenever you need it.",
"DynamicFormsExplanation": "Dynamic form & input tag helpers can create the complete form from a C# class as the model.",
"AuthenticationAuthorizationExplanation": "Rich authentication & authorization options integrated to ASP.NET Core Identity & IdentityServer4. Provides an extensible & detailed permission system.",
"CrossCuttingConcernsExplanation": "Don't repeat yourself when implementing common features. Instead, focus on your business logic and let ABP automate by convention.",
"DatabaseConnectionTransactionManagement": "Database Connection & Transaction Management",
"CorrelationIdTracking": "Correlation-Id Tracking",
"BundlingMinificationExplanation": "ABP offers a simple, dynamic, powerful, modular and built-in bundling & minification system.",
"VirtualFileSystemnExplanation": "The Virtual File System makes it possible to manage files those do not physically exist on the file system (disk). It's mainly used to embed (js, css, image, cshtml...) files into assemblies and use them like physical files on runtime.",
"ThemingExplanation": "Theming system allows to develop your application & modules theme independent by defining a set of common base libraries and layouts, based on the latest Bootstrap framework.",
"DomainDrivenDesignInfrastructureExplanation": "A complete infrastructure to build layered applications based on the Domain Driven Design patterns & principles.",
"Specification": "Specification",
"Repository": "Repository",
"DomainService": "Domain Service",
"ValueObject": "Value Object",
"ApplicationService": "Application Service",
"DataTransferObject": "Data Transfer Object",
"AggregateRootEntity": "Aggregate Root, Entity",
"AutoRESTAPIsExplanation": "ABP can automagically configure your application services as API Controllers by convention.",
"DynamicClientProxiesExplanation": "Easily consume your APIs from JavaScript and C# clients.",
"DistributedEventBusWithRabbitMQIntegrationExplanation": "Easily publish & consume distributed events using built-in Distributed Event Bus with RabbitMQ integration available.",
"TestInfrastructureExplanation": "The framework has been developed unit & integration testing in mind. Provides you base classes to make it easier. Startup templates come with pre-configured for testing.",
"AuditLoggingEntityHistoriesExplanation": "Built-in audit logging for business-critical applications. Request, service, method level audit logging and entity histories with property-level details.",
"EmailSMSAbstractionsWithTemplatingSupportExplanation": "IEmailSender and ISmsSender abstractions decouples your application logic from the infrastructure. Advanced email template system allows to create & localize email templates and easily use whenever needed.",
"LocalizationExplanation": "Localization system allows to create resources in plain JSON files and use them to localize your UI. It supports advanced scenarios like inheritance, extensions and JavaScript integration while it is fully compatible with AspNet Core's localization system.",
"SettingManagementExplanation": "Define settings for your application and get values on runtime based on the current configuration, tenant and user.",
"ExtensionMethodsHelpersExplanation": "Don't repeat yourself even for trivial code parts. Extensions & helpers for standard types makes your code much cleaner and easy to write.",
"AspectOrientedProgrammingExplanation": "Provides a comfortable infrastructure to create dynamic proxies and implement Aspect Oriented Programming. Intercept any class and execute your code before & after every method execution.",
"DependencyInjectionByConventionsExplanation": "No need to register your classes to dependency injection manually. Automatically registers common service types by convention. For other type of services, you can use interfaces and attributes to make it easier and in-place.",
"DataFilteringExplanation": "Define and use data filters those are automatically applied when you query entities from database. Soft Delete & Multi-Tenant filters are provided out of the box when you implement simple interfaces.",
"PublishEvents": "Publish Events",
"HandleEvents": "Handle Events",
"AndMore": "and more...",
"Code": "Code",
"Result": "Result",
"SeeTheDocumentForMoreInformation": "See the <a href=\"{1}\">{0} document</a> for more information",
"IndexPageHeroSection": "<span class=\"first-line shine\"><strong>open source</strong></span><span class=\"second-line text-uppercase\">Web Application<br />Framework </span><span class=\"third-line shine2\"><strong>for asp.net core</strong></span>",
"UiFramework": "UI Framework",
"EmailAddress": "Email address",
"Mobile": "Mobile",
"ReactNative": "React Native",
"Strong": "Strong",
"Complete": "Complete",
"BasedLayeringModel": "Based Layering Model",
"Microservice": "Microservice",
"Compatible": "Compatible",
"MeeTTheABPCommunityInfo": "Our mission is to create an environment where developers can help each other with articles, tutorials, case studies, etc. and meet like-minded people.",
"JoinTheABPCommunityInfo": "Get involved with a vibrant community and become a contributor to the ABP Framework!",
"AllArticles": "All Articles",
"SubmitYourArticle": "Submit Your Article",
"DynamicClientProxyDocument": "See the dynamic client proxy documentations for <a href=\"{0}\">JavaScript</a> & <a href=\"{1}\">C#</a>.",
"EmailSMSAbstractionsDocument": "See the <a href=\"{0}\">emailing</a> and <a href=\"{1}\">SMS sending</a> documents for more information.",
"CreateProjectWizard": "This wizard creates a new project from the startup template which is properly configured to jump start to your project.",
"TieredOption": "Creates a tiered solution where Web and Http API layers are physically separated. If not checked, creates a layered solution which is less complex and suitable for most scenarios.",
"SeparateIdentityServerOption": "Separates server side into two applications: First one is for the identity server and the second one is for your server side HTTP API.",
"UseslatestPreVersion": "Uses latest pre-release version",
"ReadTheDocumentation": "<span class=\"text-primary\">Read</span><span class=\"text-success\">The Documentation</span>",
"Documentation": "Documentation",
"GettingStartedTutorial": "Getting Started Tutorial",
"ApplicationDevelopmentTutorial": "Application Development Tutorial",
"TheStartupTemplate": "The Startup Template",
"InstallABPCLIInfo": "ABP CLI is the fastest way to start a new solution with the ABP framework. Install the ABP CLI using a command line window:",
"DifferentLevelOfNamespaces": "You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.",
"ABPCLIExamplesInfo": "<strong>new</strong> command creates a <strong>layered MVC application</strong> with <strong>Entity Framework Core</strong> as the database provider. However, it has additional options. Examples:",
"SeeCliDocumentForMoreInformation": "See the <a href=\"{0}\">ABP CLI document</a> for more options or select the \"Direct Download\" tab above.",
"Optional": "Optional",
"LocalFrameworkRef": "Keep local project reference for the framework packages.",
"BlobStoring": "BLOB Storing",
"BlobStoringExplanation": "BLOB Storing system provides an abstraction to work with BLOBs. ABP provides some pre-built storage provider integrations (Azure, AWS, File System, Database, etc.) that you can easily use in your applications.",
"TextTemplating": "Text Templating",
"TextTemplatingExplanation": "Text templating is used to dynamically render contents based on a template and a model (a data object). For example, you can use it to create dynamic email contents with a pre-built template.",
"MultipleUIOptions": "Multiple UI Options",
"MultipleDBOptions": "Multiple Database Providers",
"MultipleUIOptionsExplanation": "The core framework is designed as UI independent and can work with any type of UI system, while there are multiple pre-built and integrated options are provided out of the box.",
"MultipleDBOptionsExplanation": "The framework can work with any data source, while the following providers are officially developed and supported:",
"SelectLanguage": "Select language"
}
}

@ -192,6 +192,7 @@
"MultipleUIOptions": "Multiple UI Options",
"MultipleDBOptions": "Multiple Database Providers",
"MultipleUIOptionsExplanation": "The core framework is designed as UI independent and can work with any type of UI system, while there are multiple pre-built and integrated options are provided out of the box.",
"MultipleDBOptionsExplanation": "The framework can work with any data source, while the following providers are officially developed and supported;"
"MultipleDBOptionsExplanation": "The framework can work with any data source, while the following providers are officially developed and supported;",
"SelectLanguage": "Select language"
}
}

@ -184,6 +184,7 @@
"ABPCLIExamplesInfo": "<strong>nuevo</strong> comando crea una <strong>aplicación MVC por capas</strong> con <strong>Entity Framework Core</strong> como proveedor de base de datos. Sin embargo, tiene distintas opciones. Ejemplos:",
"SeeCliDocumentForMoreInformation": "Ver el <a href=\"{0}\">documento ABP CLI </a> para más opciones o selecciona la \"Direct Download\" pestaña de arriba.",
"Optional": "Opcional",
"LocalFrameworkRef": "Mantén la referencia al proyecto local para los paquetes del framework."
"LocalFrameworkRef": "Mantén la referencia al proyecto local para los paquetes del framework.",
"SelectLanguage": "Vali keel"
}
}

@ -154,6 +154,7 @@
"SeeTheDocumentForMoreInformation": "Consulte o <a href=\"{1}\">{0} documento</a> para obter mais informações",
"IndexPageHeroSection": "<span class=\"first-line shine\"><strong>código aberto</strong></span><span class=\"second-line text-uppercase\">Aplicativo da Web<br />Framework </span><span class=\"third-line shine2\"><strong>para o ASP.NET Core</strong></span>",
"UiFramework": "UI Framework",
"EmailAddress": "Endereço de email"
"EmailAddress": "Endereço de email",
"SelectLanguage": "Selecione o idioma"
}
}

@ -156,6 +156,7 @@
"UiFramework": "UI Framework",
"EmailAddress": "E-Posta Adresi",
"Mobile": "Mobil",
"ReactNative": "React Native"
"ReactNative": "React Native",
"SelectLanguage": "Dil seçin"
}
}

@ -191,6 +191,8 @@
"TextTemplatingExplanation": "文本模板是基于模板和模型(数据对象)使用动态渲染内容. 例如你可以使用预构建的模板来创建动态的电子邮件内容.",
"MultipleUIOptions": "多个UI选项",
"MultipleDBOptions": "多个数据库提供程序",
"MultipleUIOptionsExplanation": "核心框架设计为独立与UI,可以和任何类型的UI系统一起使用. 同时提供了多个开箱即用的预构建集成选项."
"MultipleUIOptionsExplanation": "核心框架设计为独立与UI,可以和任何类型的UI系统一起使用. 同时提供了多个开箱即用的预构建集成选项.",
"MultipleDBOptionsExplanation": "该框架可以使用任何数据源,并且以下提供程序已得到正式开发和支持;",
"SelectLanguage": "选择语言"
}
}
}

@ -154,6 +154,7 @@
"SeeTheDocumentForMoreInformation": "查看<a href=\"{1}\">{0} 文件</a>獲得更多訊息",
"IndexPageHeroSection": "<span class=\"third-line shine2\"><strong>asp.net core的</strong></span><span class=\"first-line shine\"><strong>開源</strong></span><span class=\"second-line text-uppercase\">Web應用程式<br />框架 </span>",
"UiFramework": "UI框架",
"EmailAddress": "電子信箱地址"
"EmailAddress": "電子信箱地址",
"SelectLanguage": "选择语言"
}
}

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>4.1.2</Version>
<Version>4.2.0</Version>
<NoWarn>$(NoWarn);CS1591;CS0436</NoWarn>
<PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl>
<PackageProjectUrl>https://abp.io/</PackageProjectUrl>
@ -10,10 +10,8 @@
<RepositoryUrl>https://github.com/abpframework/abp/</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- https://github.com/dotnet/sourcelink -->
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<!-- Include symbol files (*.pdb) in the built .nupkg -->
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0-beta-20204-02">

@ -43,12 +43,14 @@ public class Book : AggregateRoot<Guid>
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException($"name can not be empty or white space!");
throw new ArgumentException(
$"name can not be empty or white space!");
}
if (name.Length > MaxNameLength)
{
throw new ArgumentException($"name can not be longer than {MaxNameLength} chars!");
throw new ArgumentException(
$"name can not be longer than {MaxNameLength} chars!");
}
return name;
@ -349,8 +351,9 @@ public class DistrictAppService
protected async override Task<District> GetEntityByIdAsync(DistrictKey id)
{
var queryable = await Repository.GetQueryableAsync();
return await AsyncQueryableExecuter.FirstOrDefaultAsync(
Repository.Where(d => d.CityId == id.CityId && d.Name == id.Name)
queryable.Where(d => d.CityId == id.CityId && d.Name == id.Name)
);
}
}

@ -184,7 +184,7 @@ public class PersonAppService : ApplicationService, IPersonAppService
}
````
ABP Framework's repository doesn't have this method. Instead, it implements the `IQueryable` itself. So, you can directly use LINQ on the repository:
ABP Framework's repository have `GetQueryableAsync` instead:
````csharp
public class PersonAppService : ApplicationService, IPersonAppService
@ -198,14 +198,15 @@ public class PersonAppService : ApplicationService, IPersonAppService
public async Task DoIt()
{
var people = await _personRepository
var queryable = await _personRepository.GetQueryableAsync();
var people = await queryable
.Where(p => p.BirthYear > 2000) //Use LINQ extension methods
.ToListAsync();
}
}
````
> Note that in order to use the async LINQ extension methods (like `ToListAsync` here), you may need to depend on the database provider (like EF Core) since these methods are defined in the database provider package, they are not standard LINQ methods.
> Note that in order to use the async LINQ extension methods (like `ToListAsync` here), you may need to depend on the database provider (like EF Core) since these methods are defined in the database provider package, they are not standard LINQ methods. See the [repository document](Repositories.md) for alternative approaches for async query execution.
#### FirstOrDefault(predicate), Single()... Methods

@ -80,7 +80,7 @@ public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase
## Register Background Worker
After creating a background worker class, you should to add it to the `IBackgroundWorkerManager`. The most common place is the `OnApplicationInitialization` method of your module class:
After creating a background worker class, you should add it to the `IBackgroundWorkerManager`. The most common place is the `OnApplicationInitialization` method of your module class:
````csharp
[DependsOn(typeof(AbpBackgroundWorkersModule))]

@ -144,7 +144,7 @@ public virtual async Task<IdentityUser> FindByNormalizedUserNameAsync(
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return await DbSet
return await (await GetDbSetAsync())
.IncludeDetails(includeDetails)
.FirstOrDefaultAsync(
u => u.NormalizedUserName == normalizedUserName,
@ -175,14 +175,15 @@ public static IQueryable<IdentityUser> IncludeDetails(
}
````
* **Do** use the `IncludeDetails` extension method in the repository methods just like used in the example code above (see FindByNormalizedUserNameAsync).
* **Do** use the `IncludeDetails` extension method in the repository methods just like used in the example code above (see `FindByNormalizedUserNameAsync`).
- **Do** override `WithDetails` method of the repository for aggregates root which have **sub collections**. Example:
````C#
public override IQueryable<IdentityUser> WithDetails()
public override async Task<IQueryable<IdentityUser>> WithDetailsAsync()
{
return GetQueryable().IncludeDetails(); // Uses the extension method defined above
// Uses the extension method defined above
return (await GetQueryableAsync()).IncludeDetails();
}
````

@ -128,7 +128,7 @@ public async Task<IdentityUser> FindByNormalizedUserNameAsync(
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return await GetMongoQueryable()
return await (await GetMongoQueryableAsync())
.FirstOrDefaultAsync(
u => u.NormalizedUserName == normalizedUserName,
GetCancellationToken(cancellationToken)
@ -139,8 +139,8 @@ public async Task<IdentityUser> FindByNormalizedUserNameAsync(
`GetCancellationToken` fallbacks to the `ICancellationTokenProvider.Token` to obtain the cancellation token if it is not provided by the caller code.
* **Do** ignore the `includeDetails` parameters for the repository implementation since MongoDB loads the aggregate root as a whole (including sub collections) by default.
* **Do** use the `GetMongoQueryable()` method to obtain an `IQueryable<TEntity>` to perform queries wherever possible. Because;
* `GetMongoQueryable()` method automatically uses the `ApplyDataFilters` method to filter the data based on the current data filters (like soft delete and multi-tenancy).
* **Do** use the `GetMongoQueryableAsync()` method to obtain an `IQueryable<TEntity>` to perform queries wherever possible. Because;
* `GetMongoQueryableAsync()` method automatically uses the `ApplyDataFilters` method to filter the data based on the current data filters (like soft delete and multi-tenancy).
* Using `IQueryable<TEntity>` makes the code as much as similar to the EF Core repository implementation and easy to write and read.
* **Do** implement data filtering if it is not possible to use the `GetMongoQueryable()` method.

@ -42,24 +42,6 @@ Task<IdentityUser> FindByNormalizedUserNameAsync(
);
````
* **Do** create a **synchronous extension** method for each asynchronous repository method. Example:
````C#
public static class IdentityUserRepositoryExtensions
{
public static IdentityUser FindByNormalizedUserName(
this IIdentityUserRepository repository,
[NotNull] string normalizedUserName)
{
return AsyncHelper.RunSync(
() => repository.FindByNormalizedUserNameAsync(normalizedUserName)
);
}
}
````
This will allow synchronous code to use the repository methods easier.
* **Do** add an optional `bool includeDetails = true` parameter (default value is `true`) for every repository method which returns a **single entity**. Example:
````C#

@ -55,7 +55,7 @@ Configure<AbpBlobStoringOptions>(options =>
* **RoleSessionName** ([NotNull]string): Used to identify the temporary access credentials, it is recommended to use different application users to distinguish.
* **Policy** (string): Additional permission restrictions. See the [document](https://help.aliyun.com/document_detail/100680.html) for details.
* **DurationSeconds** (int): Validity period(s) of a temporary access certificate,minimum is 900 and the maximum is 3600.
* **ContainerName** (string): You can specify the container name in Aliyun. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aliyun has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://help.aliyun.com/knowledge_detail/39668.html):
* **ContainerName** (string): You can specify the container name in Aliyun. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aliyun has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://help.aliyun.com/knowledge_detail/39668.html):
* Container names must start or end with a letter or number, and can contain only letters, numbers, and the dash (-) character.
* Container names Must start and end with lowercase letters and numbers.
* Container names must be from **3** through **63** characters long.

@ -60,7 +60,7 @@ Configure<AbpBlobStoringOptions>(options =>
* **Region** (string): The system name of the service.
* **Policy** (string): An IAM policy in JSON format that you want to use as an inline session policy.
* **DurationSeconds** (int): Validity period(s) of a temporary access certificate,minimum is 900 and the maximum is 3600. **note**: Using subaccounts operated OSS,if the value is 0.
* **ContainerName** (string): You can specify the container name in Aws. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aws has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html):
* **ContainerName** (string): You can specify the container name in Aws. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aws has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html):
* Bucket names must be between **3** and **63** characters long.
* Bucket names can consist only of **lowercase** letters, numbers, dots (.), and hyphens (-).
* Bucket names must begin and end with a letter or number.

@ -43,7 +43,7 @@ Configure<AbpBlobStoringOptions>(options =>
* **EndPoint** (string): URL to object storage service. Please refer to MinIO Client SDK for .NET: https://docs.min.io/docs/dotnet-client-quickstart-guide.html
* **AccessKey** (string): Access key is the user ID that uniquely identifies your account.
* **SecretKey** (string): Secret key is the password to your account.
* **BucketName** (string): You can specify the bucket name in MinIO. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)).MinIO is the defacto standard for S3 compatibility, So MinIO has some **rules for naming bucket**. The [following rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html) apply for naming MinIO buckets:
* **BucketName** (string): You can specify the bucket name in MinIO. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)).MinIO is the defacto standard for S3 compatibility, So MinIO has some **rules for naming bucket**. The [following rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html) apply for naming MinIO buckets:
* Bucket names must be between **3** and **63** characters long.
* Bucket names can consist only of **lowercase** letters, numbers, dots (.), and hyphens (-).
* Bucket names must begin and end with a letter or number.

@ -21,7 +21,7 @@ The ABP Framework has already the following storage provider implementations:
* [Azure](Blob-Storing-Azure.md): Stores BLOBs on the [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/).
* [Aliyun](Blob-Storing-Aliyun.md): Stores BLOBs on the [Aliyun Storage Service](https://help.aliyun.com/product/31815.html).
* [Minio](Blob-Storing-Minio.md): Stores BLOBs on the [MinIO Object storage](https://min.io/).
* [Aws](Blob-Storing-Aws.md): Stores BLOBs on the[Amazon Simple Storage Service](https://aws.amazon.com/s3/).
* [Aws](Blob-Storing-Aws.md): Stores BLOBs on the [Amazon Simple Storage Service](https://aws.amazon.com/s3/).
More providers will be implemented by the time. You can [request](https://github.com/abpframework/abp/issues/new) it for your favorite provider or [create it yourself](Blob-Storing-Custom-Provider.md) and [contribute](Contribution/Index.md) to the ABP Framework.

@ -0,0 +1,51 @@
# ABP.IO Platform 4.1 Final Has Been Released!
[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 4.1 versions have been released today.
## What's New With 4.1?
Since all the new features are already explained in details with the [4.1 RC Announcement Post](https://blog.abp.io/abp/ABP.IO-Platform-v4.1-RC-Has-Been-Released), I will not repeat all the details again. See the [RC Blog Post](https://blog.abp.io/abp/ABP.IO-Platform-v4.1-RC-Has-Been-Released) for all the features and enhancements.
## Creating New Solutions
You can create a new solution with the ABP Framework version 4.1 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started).
> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for details.
## How to Upgrade an Existing Solution
### Install/Update the ABP CLI
First of all, install the ABP CLI or upgrade to the latest version.
If you haven't installed yet:
```bash
dotnet tool install -g Volo.Abp.Cli
```
To update an existing installation:
```bash
dotnet tool update -g Volo.Abp.Cli
```
### ABP UPDATE Command
[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides a handy command to update all the ABP related NuGet and NPM packages in your solution with a single command:
```bash
abp update
```
Run this command in the root folder of your solution.
## ABP Community
We started to get more contributions by the community for the [ABP Community](https://community.abp.io/) contents. Thank you all!
We will be adding **Video Content** sharing system in a short time. We are planning to create short video contents, especially to explore the new features in every release. Again, we will be waiting video contributions by the community :)
## About the Next Versions
Planned preview date for the version **4.2 is January 14, 2021**. See the [Road Map](https://docs.abp.io/en/abp/latest/Road-Map) document and [GitHub Milestones](https://github.com/abpframework/abp/milestones) to learn what's planned for the next versions. We are trying to be clear about the coming features and the next release dates.

@ -105,7 +105,13 @@ abp new Acme.BookStore
* `--preview`: Use latest preview version.
* `--template-source` or `-ts`: Specifies a custom template source to use to build the project. Local and network sources can be used(Like `D:\local-template` or `https://.../my-template-file.zip`).
* `--create-solution-folder` or `-csf`: Specifies if the project will be in a new folder in the output folder or directly the output folder.
* `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true` for EF Core and it is configured to use the SQL Server. If you want to use the EF Core, but need to change the DBMS, you can change it as [described here](Entity-Framework-Core-Other-DBMS.md) (after creating the solution).
* `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True` for EF Core and it is configured to use the SQL Server. If you want to use the EF Core, but need to change the DBMS, you can change it as [described here](Entity-Framework-Core-Other-DBMS.md) (after creating the solution).
* `--database-management-system` or `-dbms`: Sets the database management system. Default is **SQL Server**. Supported DBMS's:
* `SqlServer`
* `MySQL`
* `SQLite`
* `Oracle-Devart`
* `PostgreSQL`
* `--local-framework-ref --abp-path`: Uses local projects references to the ABP framework instead of using the NuGet packages. This can be useful if you download the ABP Framework source code and have a local reference to the framework from your application.
* `--no-random-port`: Uses template's default ports.

@ -11,157 +11,75 @@ When you use HTTP on your Identity Server 4 enabled website, users may not login
Create the below extension in your ***.Web** project.
```csharp
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Extensions.DependencyInjection
{
public static class SameSiteCookiesServiceCollectionExtensions
{
/// <summary>
/// -1 defines the unspecified value, which tells ASPNET Core to NOT
/// send the SameSite attribute. With ASPNET Core 3.1 the
/// <seealso cref="SameSiteMode" /> enum will have a definition for
/// Unspecified.
/// </summary>
private const SameSiteMode Unspecified = (SameSiteMode)(-1);
/// <summary>
/// Configures a cookie policy to properly set the SameSite attribute
/// for Browsers that handle unknown values as Strict. Ensure that you
/// add the <seealso cref="Microsoft.AspNetCore.CookiePolicy.CookiePolicyMiddleware" />
/// into the pipeline before sending any cookies!
/// </summary>
/// <remarks>
/// Minimum ASPNET Core Version required for this code:
/// - 2.1.14
/// - 2.2.8
/// - 3.0.1
/// - 3.1.0-preview1
/// Starting with version 80 of Chrome (to be released in February 2020)
/// cookies with NO SameSite attribute are treated as SameSite=Lax.
/// In order to always get the cookies send they need to be set to
/// SameSite=None. But since the current standard only defines Lax and
/// Strict as valid values there are some browsers that treat invalid
/// values as SameSite=Strict. We therefore need to check the browser
/// and either send SameSite=None or prevent the sending of SameSite=None.
/// Relevant links:
/// - https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1
/// - https://tools.ietf.org/html/draft-west-cookie-incrementalism-00
/// - https://www.chromium.org/updates/same-site
/// - https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
/// - https://bugs.webkit.org/show_bug.cgi?id=198181
/// </remarks>
/// <param name="services">The service collection to register <see cref="CookiePolicyOptions" /> into.</param>
/// <returns>The modified <see cref="IServiceCollection" />.</returns>
public static IServiceCollection ConfigureNonBreakingSameSiteCookies(this IServiceCollection services)
public static IServiceCollection AddSameSiteCookiePolicy(this IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = Unspecified;
options.OnAppendCookie = cookieContext =>
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
options.OnDeleteCookie = cookieContext =>
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
options.OnAppendCookie = cookieContext =>
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
options.OnDeleteCookie = cookieContext =>
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
});
return services;
}
private static void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
if (options.SameSite == SameSiteMode.None)
{
var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
if (DisallowsSameSiteNone(userAgent))
if (!httpContext.Request.IsHttps || DisallowsSameSiteNone(userAgent))
{
options.SameSite = Unspecified;
// For .NET Core < 3.1 set SameSite = (SameSiteMode)(-1)
options.SameSite = SameSiteMode.Unspecified;
}
}
}
/// <summary>
/// Checks if the UserAgent is known to interpret an unknown value as Strict.
/// For those the <see cref="CookieOptions.SameSite" /> property should be
/// set to <see cref="Unspecified" />.
/// </summary>
/// <remarks>
/// This code is taken from Microsoft:
/// https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
/// </remarks>
/// <param name="userAgent">The user agent string to check.</param>
/// <returns>Whether the specified user agent (browser) accepts SameSite=None or not.</returns>
private static bool DisallowsSameSiteNone(string userAgent)
{
// Cover all iOS based browsers here. This includes:
// - Safari on iOS 12 for iPhone, iPod Touch, iPad
// - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
// - Chrome on iOS 12 for iPhone, iPod Touch, iPad
// All of which are broken by SameSite=None, because they use the
// iOS networking stack.
// Notes from Thinktecture:
// Regarding https://caniuse.com/#search=samesite iOS versions lower
// than 12 are not supporting SameSite at all. Starting with version 13
// unknown values are NOT treated as strict anymore. Therefore we only
// need to check version 12.
if (userAgent.Contains("CPU iPhone OS 12")
|| userAgent.Contains("iPad; CPU OS 12"))
// - Safari on iOS 12 for iPhone, iPod Touch, iPad
// - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
// - Chrome on iOS 12 for iPhone, iPod Touch, iPad
// All of which are broken by SameSite=None, because they use the iOS networking stack
if (userAgent.Contains("CPU iPhone OS 12") || userAgent.Contains("iPad; CPU OS 12"))
{
return true;
}
// Cover Mac OS X based browsers that use the Mac OS networking stack.
// This includes:
// - Safari on Mac OS X.
// Cover Mac OS X based browsers that use the Mac OS networking stack. This includes:
// - Safari on Mac OS X.
// This does not include:
// - Chrome on Mac OS X
// because they do not use the Mac OS networking stack.
// Notes from Thinktecture:
// Regarding https://caniuse.com/#search=samesite MacOS X versions lower
// than 10.14 are not supporting SameSite at all. Starting with version
// 10.15 unknown values are NOT treated as strict anymore. Therefore we
// only need to check version 10.14.
if (userAgent.Contains("Safari")
&& userAgent.Contains("Macintosh; Intel Mac OS X 10_14")
&& userAgent.Contains("Version/"))
// - Chrome on Mac OS X
// Because they do not use the Mac OS networking stack.
if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") &&
userAgent.Contains("Version/") && userAgent.Contains("Safari"))
{
return true;
}
// Cover Chrome 50-69, because some versions are broken by SameSite=None
// Cover Chrome 50-69, because some versions are broken by SameSite=None,
// and none in this range require it.
// Note: this covers some pre-Chromium Edge versions,
// Note: this covers some pre-Chromium Edge versions,
// but pre-Chromium Edge does not require SameSite=None.
// Notes from Thinktecture:
// We can not validate this assumption, but we trust Microsofts
// evaluation. And overall not sending a SameSite value equals to the same
// behavior as SameSite=None for these old versions anyways.
if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
{
return true;
}
if (GetChromeVersion(userAgent) >= 80)
{
return true;
}
return false;
}
private static int GetChromeVersion(string userAgent)
{
try
{
return Convert.ToInt32(userAgent.Split("Chrome/")[1].Split('.')[0]);
}
catch (Exception)
{
return 0;
}
}
}
}
```
@ -173,7 +91,7 @@ Assume that your project name is *Acme.BookStore*. Then open `AcmeBookStoreWebMo
Add the following line to `ConfigureServices()` method.
```csharp
context.Services.ConfigureNonBreakingSameSiteCookies();
context.Services.AddSameSiteCookiePolicy(); // cookie policy to deal with temporary browser incompatibilities
```
### Step-3
@ -195,18 +113,14 @@ public override void OnApplicationInitialization(ApplicationInitializationContex
app.UseHsts();
}
app.UseCookiePolicy(); //<--- added this --->
app.UseCookiePolicy(); // added this, Before UseAuthentication or anything else that writes cookies.
//....
}
```
It's all! You are ready to go!
---
Referenced from https://www.thinktecture.com/en/identity/samesite/prepare-your-identityserver/
Referenced from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/

@ -52,7 +52,10 @@ abp new TelerikComponents --ui blazor --database-provider ef
```html
...
<link rel="stylesheet" href="https:unpkg.com/@progress/kendo-theme-default@latest/dist/all.css" />
<link rel="stylesheet" href="_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css" />
<!-- For Trial licenses use
<link rel="stylesheet" href="_content/Telerik.UI.for.Blazor.Trial/css/kendo-theme-default/all.css" />
-->
<script src="_content/Telerik.UI.for.Blazor/js/telerik-blazor.js" defer></script>
<!-- For Trial licenses use
<script src="_content/Telerik.UI.for.Blazor.Trial/js/telerik-blazor.js" defer></script>

@ -260,7 +260,7 @@ import { MatFormFieldModule } from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";
import { MatSelectModule } from "@angular/material/select";
import { MatIconModule } from "@angular/material/icon";
import { MatNativeDateModule } from '@angular/material/core';
import { MatNativeDateModule } from "@angular/material/core";
@NgModule({
imports: [
@ -1576,6 +1576,10 @@ Final UI looks as shown below:
![Author With Books](./author-with-books.gif)
## The Source Code
You can download the source code from [here](https://github.com/abpframework/abp-samples/tree/master/AcmeBookStoreAngularMaterial).
## Conclusion
We implemented Angular Material Components to our angular application which was created with ABP Framework. There is no blocker case of using angular libraries with the ABP framework.

@ -10,7 +10,7 @@ If you want to write **articles** or **how to guides** related to the ABP Framew
You can always send pull requests to the GitHub repository.
- Clone the [ABP repository](https://github.com/abpframework/abp/) from GitHub.
- [Fork](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo) the [ABP repository](https://github.com/abpframework/abp/) from GitHub.
- Build the repository using the `/build/build-all.ps1 -f` for one time.
- Make the necessary changes, including unit/integration tests.
- Send a pull request.

@ -1,6 +1,6 @@
# Dependency Injection
ABP's Dependency Injection system is developed based on Microsoft's [dependency injection extension](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) library (Microsoft.Extensions.DependencyInjection nuget package). So, it's documentation is valid in ABP too.
ABP's Dependency Injection system is developed based on Microsoft's [dependency injection extension](https://medium.com/volosoft/asp-net-core-dependency-injection-best-practices-tips-tricks-c6e9c67f9d96) library (Microsoft.Extensions.DependencyInjection nuget package). So, it's documentation is valid in ABP too.
> While ABP has no core dependency to any 3rd-party DI provider, it's required to use a provider that supports dynamic proxying and some other advanced features to make some ABP features properly work. Startup templates come with Autofac installed. See [Autofac integration](Autofac-Integration.md) document for more information.

@ -754,7 +754,8 @@ namespace IssueTracking.Issues
{
var daysAgo30 = DateTime.Now.Subtract(TimeSpan.FromDays(30));
return await DbSet.Where(i =>
var dbSet = await GetDbSetAsync();
return await dbSet.Where(i =>
//Open
!i.IsClosed &&
@ -906,7 +907,8 @@ public class EfCoreIssueRepository :
public async Task<List<Issue>> GetIssuesAsync(ISpecification<Issue> spec)
{
return await DbSet
var dbSet = await GetDbSetAsync();
return await dbSet
.Where(spec.ToExpression())
.ToListAsync();
}
@ -952,8 +954,9 @@ public class IssueAppService : ApplicationService, IIssueAppService
public async Task DoItAsync()
{
var queryable = await _issueRepository.GetQueryableAsync();
var issues = AsyncExecuter.ToListAsync(
_issueRepository.Where(new InActiveIssueSpecification())
queryable.Where(new InActiveIssueSpecification())
);
}
}
@ -996,8 +999,9 @@ public class IssueAppService : ApplicationService, IIssueAppService
public async Task DoItAsync(Guid milestoneId)
{
var queryable = await _issueRepository.GetQueryableAsync();
var issues = AsyncExecuter.ToListAsync(
_issueRepository
queryable
.Where(
new InActiveIssueSpecification()
.And(new MilestoneSpecification(milestoneId))

@ -223,7 +223,7 @@ Pathes of the templates in the virtual file system are shown below:
* `/Volo/Abp/Emailing/Templates/Layout.tpl`
* `/Volo/Abp/Emailing/Templates/Message.tpl`
If you add files to the same localization in the virtual file system, your files will override them.
If you add files to the same location in the virtual file system, your files will override them.
Templates are inline localized, that means you can take the power of the [localization system](Localization.md) to make your templates multi-cultural.
@ -247,4 +247,4 @@ So, don't confuse if you don't receive emails on DEBUG mode. Emails will be sent
## See Also
* [MailKit integration for sending emails](MailKit.md)
* [MailKit integration for sending emails](MailKit.md)

@ -586,7 +586,7 @@ First step is to change the connection string section inside all the `appsetting
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True"
}
````
@ -594,10 +594,10 @@ Change it as shown below:
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true",
"AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true",
"AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true",
"AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True",
"AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True"
}
````

@ -236,7 +236,8 @@ public class BookRepository
public async Task DeleteBooksByType(BookType type)
{
await DbContext.Database.ExecuteSqlRawAsync(
var dbContext = await GetDbContextAsync();
await dbContext.Database.ExecuteSqlRawAsync(
$"DELETE FROM Books WHERE Type = {(int)type}"
);
}
@ -344,7 +345,7 @@ You have different options when you want to load the related entities while quer
#### Repository.WithDetails
`IRepository.WithDetails(...)` can be used to include one relation collection/property to the query.
`IRepository.WithDetailsAsync(...)` can be used to get an `IQueryable<T>` by including one relation collection/property.
**Example: Get an order with lines**
@ -355,7 +356,7 @@ using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services;
namespace MyCrm
namespace AbpDemo.Orders
{
public class OrderManager : DomainService
{
@ -368,35 +369,39 @@ namespace MyCrm
public async Task TestWithDetails(Guid id)
{
var query = _orderRepository
.WithDetails(x => x.Lines)
.Where(x => x.Id == id);
//Get a IQueryable<T> by including sub collections
var queryable = await _orderRepository.WithDetailsAsync(x => x.Lines);
//Apply additional LINQ extension methods
var query = queryable.Where(x => x.Id == id);
//Execute the query and get the result
var order = await AsyncExecuter.FirstOrDefaultAsync(query);
}
}
}
````
> `AsyncExecuter` is used to execute async LINQ extensions without depending on the EF Core. If you add EF Core NuGet package reference to your project, then you can directly use `await _orderRepository.WithDetails(x => x.Lines).FirstOrDefaultAsync()`. But, this time you depend on the EF Core in your domain layer. See the [repository document](Repositories.md) to learn more.
> `AsyncExecuter` is used to execute async LINQ extensions without depending on the EF Core. If you add EF Core NuGet package reference to your project, then you can directly use `await query.FirstOrDefaultAsync()`. But, this time you depend on the EF Core in your domain layer. See the [repository document](Repositories.md) to learn more.
**Example: Get a list of orders with their lines**
````csharp
public async Task TestWithDetails()
{
var query = _orderRepository
.WithDetails(x => x.Lines);
//Get a IQueryable<T> by including sub collections
var queryable = await _orderRepository.WithDetailsAsync(x => x.Lines);
var orders = await AsyncExecuter.ToListAsync(query);
//Execute the query and get the result
var orders = await AsyncExecuter.ToListAsync(queryable);
}
````
> `WithDetails` method can get more than one expression parameter if you need to include more than one navigation property or collection.
> `WithDetailsAsync` method can get more than one expression parameter if you need to include more than one navigation property or collection.
#### DefaultWithDetailsFunc
If you don't pass any expression to the `WithDetails` method, then it includes all the details using the `DefaultWithDetailsFunc` option you provide.
If you don't pass any expression to the `WithDetailsAsync` method, then it includes all the details using the `DefaultWithDetailsFunc` option you provide.
You can configure `DefaultWithDetailsFunc` for an entity in the `ConfigureServices` method of your [module](Module-Development-Basics.md) in your `EntityFrameworkCore` project.
@ -419,12 +424,15 @@ Then you can use the `WithDetails` without any parameter:
````csharp
public async Task TestWithDetails()
{
var query = _orderRepository.WithDetails();
var orders = await AsyncExecuter.ToListAsync(query);
//Get a IQueryable<T> by including all sub collections
var queryable = await _orderRepository.WithDetailsAsync();
//Execute the query and get the result
var orders = await AsyncExecuter.ToListAsync(queryable);
}
````
`WithDetails()` executes the expression you've setup as the `DefaultWithDetailsFunc`.
`WithDetailsAsync()` executes the expression you've setup as the `DefaultWithDetailsFunc`.
#### Repository Get/Find Methods
@ -466,7 +474,7 @@ public async Task TestWithDetails()
#### Alternatives
The repository patters tries to encapsulate the EF Core, so your options are limited. If you need an advanced scenario, you can follow one of the options;
The repository pattern tries to encapsulate the EF Core, so your options are limited. If you need an advanced scenario, you can follow one of the options;
* Create a custom repository method and use the complete EF Core API.
* Reference to the `Volo.Abp.EntityFrameworkCore` package from your project. In this way, you can directly use `Include` and `ThenInclude` in your code.
@ -550,24 +558,15 @@ See also [lazy loading document](https://docs.microsoft.com/en-us/ef/core/queryi
In most cases, you want to hide EF Core APIs behind a repository (this is the main purpose of the repository pattern). However, if you want to access the `DbContext` instance over the repository, you can use `GetDbContext()` or `GetDbSet()` extension methods. Example:
````csharp
public class BookService
public async Task TestAsync()
{
private readonly IRepository<Book, Guid> _bookRepository;
public BookService(IRepository<Book, Guid> bookRepository)
{
_bookRepository = bookRepository;
}
public void Foo()
{
DbContext dbContext = _bookRepository.GetDbContext();
DbSet<Book> books = _bookRepository.GetDbSet();
}
var dbContext = await _orderRepository.GetDbContextAsync();
var dbSet = await _orderRepository.GetDbSetAsync();
//var dbSet = dbContext.Set<Order>(); //Alternative, when you have the DbContext
}
````
* `GetDbContext` returns a `DbContext` reference instead of `BookStoreDbContext`. You can cast it, however in most cases you don't need it.
* `GetDbContextAsync` returns a `DbContext` reference instead of `BookStoreDbContext`. You can cast it if you need. However, you don't need it in most cases.
> Important: You must reference to the `Volo.Abp.EntityFrameworkCore` package from the project you want to access to the `DbContext`. This breaks encapsulation, but this is what you want in that case.
@ -735,6 +734,52 @@ Configure<AbpDbContextOptions>(options =>
});
````
### Customize Bulk Operations
If you have better logic or using an external library for bulk operations, you can override the logic via implementing`IEfCoreBulkOperationProvider`.
- You may use example template below:
```csharp
public class MyCustomEfCoreBulkOperationProvider
: IEfCoreBulkOperationProvider, ITransientDependency
{
public async Task DeleteManyAsync<TDbContext, TEntity>(
IEfCoreRepository<TEntity> repository,
IEnumerable<TEntity> entities,
bool autoSave,
CancellationToken cancellationToken)
where TDbContext : IEfCoreDbContext
where TEntity : class, IEntity
{
// Your logic here.
}
public async Task InsertManyAsync<TDbContext, TEntity>(
IEfCoreRepository<TEntity> repository,
IEnumerable<TEntity> entities,
bool autoSave,
CancellationToken cancellationToken)
where TDbContext : IEfCoreDbContext
where TEntity : class, IEntity
{
// Your logic here.
}
public async Task UpdateManyAsync<TDbContext, TEntity>(
IEfCoreRepository<TEntity> repository,
IEnumerable<TEntity> entities,
bool autoSave,
CancellationToken cancellationToken)
where TDbContext : IEfCoreDbContext
where TEntity : class, IEntity
{
// Your logic here.
}
}
```
## See Also
* [Entities](Entities.md)
* [Repositories](Repositories.md)

@ -25,53 +25,41 @@ Check the **connection string** in the `appsettings.json` file under the {{if Ti
}
````
The solution is configured to use **Entity Framework Core** with **MS SQL Server** by default. EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers, so you can use any supported DBMS. See [the Entity Framework integration document](Entity-Framework-Core.md) to learn how to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md).
### Apply the Migrations
The solution uses the [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). So, you need to apply migrations to create the database. There are two ways of applying the database migrations.
> **About the Connection Strings and Database Management Systems**
>
> The solution is configured to use **Entity Framework Core** with **MS SQL Server** by default. However, if you've selected another DBMS using the `-dbms` parameter on the ABP CLI `new` command (like `-dbms MySQL`), the connection string might be different for you.
>
> EF Core supports [various](https://docs.microsoft.com/en-us/ef/core/providers/) database providers and you can use any supported DBMS. See [the Entity Framework integration document](Entity-Framework-Core.md) to learn how to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md) if you need later.
#### Apply Migrations Using the DbMigrator
### Database Migrations
The solution comes with a `.DbMigrator` console application which applies migrations and also **seeds the initial data**. It is useful on **development** as well as on **production** environment.
The solution uses the [Entity Framework Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli). It comes with a `.DbMigrator` console application which **applies the migrations** and also **seeds the initial data**. It is useful on **development** as well as on **production** environment.
> `.DbMigrator` project has its own `appsettings.json`. So, if you have changed the connection string above, you should also change this one.
Right click to the `.DbMigrator` project and select **Set as StartUp Project**
![set-as-startup-project](images/set-as-startup-project.png)
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below:
### The Initial Migration
![db-migrator-output](images/db-migrator-output.png)
> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database (with the password is `1q2w3E*`) which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database.
`.DbMigrator` application automatically **creates the Initial migration** on first run.
#### Using EF Core Update-Database Command
**If you are using Visual Studio, you can skip to the *Running the DbMigrator* section.** However, other IDEs (e.g. Rider) may have problems for the first run since it adds the initial migration and compiles the project. In this case, open a command line terminal in the folder of the `.DbMigrator` project and run the following command:
Ef Core has `Update-Database` command which creates database if necessary and applies pending migrations.
{{ if UI == "MVC" }}
Right click to the {{if Tiered == "Yes"}}`.IdentityServer`{{else}}`.Web`{{end}} project and select **Set as StartUp project**:
````bash
dotnet run
````
{{ else if UI != "MVC" }}
For the next time, you can just run it in your IDE as you normally do.
Right click to the `.HttpApi.Host` project and select **Set as StartUp Project**:
### Running the DbMigrator
{{ end }}
Right click to the `.DbMigrator` project and select **Set as StartUp Project**
![set-as-startup-project](images/set-as-startup-project.png)
Open the **Package Manager Console**, select `.EntityFrameworkCore.DbMigrations` project as the **Default Project** and run the `Update-Database` command:
![package-manager-console-update-database](images/package-manager-console-update-database.png)
Hit F5 (or Ctrl+F5) to run the application. It will have an output like shown below:
This will create a new database based on the configured connection string.
![db-migrator-output](images/db-migrator-output.png)
> **Using the `.DbMigrator` tool is the suggested way**, because it also seeds the initial data to be able to properly run the web application.
>
> If you just use the `Update-Database` command, you will have an empty database, so you can not login to the application since there is no initial admin user in the database. You can use the `Update-Database` command in development time when you don't need to seed the database. However, using the `.DbMigrator` application is easier and you can always use it to migrate the schema and seed the database.
> Initial [seed data](Data-Seeding.md) creates the `admin` user in the database (with the password is `1q2w3E*`) which is then used to login to the application. So, you need to use `.DbMigrator` at least once for a new database.
{{ else if DB == "Mongo" }}

@ -0,0 +1,101 @@
# ABP version 4.2 Migration Guide
This version has no breaking changes but there is an important change on the repositories that should be applied for your application for an important performance and scalability gain.
## IRepository.GetQueryableAsync
`IRepository` interface inherits `IQueryable`, so you can directly use the standard LINQ extension methods, like `Where`, `OrderBy`, `First`, `Sum`... etc.
**Example: Using LINQ directly over the repository object**
````csharp
public class BookAppService : ApplicationService, IBookAppService
{
private readonly IRepository<Book, Guid> _bookRepository;
public BookAppService(IRepository<Book, Guid> bookRepository)
{
_bookRepository = bookRepository;
}
public async Task DoItInOldWayAsync()
{
//Apply any standard LINQ extension method
var query = _bookRepository
.Where(x => x.Price > 10)
.OrderBy(x => x.Name);
//Execute the query asynchronously
var books = await AsyncExecuter.ToListAsync(query);
}
}
````
*See [the documentation](https://docs.abp.io/en/abp/4.2/Repositories#iqueryable-async-operations) if you wonder what is the `AsyncExecuter`.*
**Beginning from the version 4.2, the recommended way is using `IRepository.GetQueryableAsync()` to obtain an `IQueryable`, then use the LINQ extension methods over it.**
**Example: Using the new GetQueryableAsync method**
````csharp
public async Task DoItInNewWayAsync()
{
//Use GetQueryableAsync to obtain the IQueryable<Book> first
var queryable = await _bookRepository.GetQueryableAsync();
//Then apply any standard LINQ extension method
var query = queryable
.Where(x => x.Price > 10)
.OrderBy(x => x.Name);
//Finally, execute the query asynchronously
var books = await AsyncExecuter.ToListAsync(query);
}
````
ABP may start a database transaction when you get an `IQueryable` (If current [Unit Of Work](https://docs.abp.io/en/abp/latest/Unit-Of-Work) is transactional). In this new way, it is possible to **start the database transaction in an asynchronous way**. Previously, we could not get the advantage of asynchronous while starting the transactions.
> **The new way has a significant performance and scalability gain. The old usage (directly using LINQ over the repositories) will be removed in the next major version (5.0).** You have a lot of time for the change, but we recommend to immediately take the action since the old usage has a big **scalability problem**.
### Actions to Take
* Use the repository's queryable feature as explained before.
* If you've overridden `CreateFilteredQuery` in a class derived from `CrudAppService`, you should override the `CreateFilteredQueryAsync` instead and remove the `CreateFilteredQuery` in your class.
* If you've overridden `WithDetails` in your custom repositories, remove it and override `WithDetailsAsync` instead.
* If you've used `DbContext` or `DbSet` properties in your custom repositories, use `GetDbContextAsync()` and `GetDbSetAsync()` methods instead of them.
You can re-build your solution and check the `Obsolete` warnings to find some of the usages need to change.
#### About IRepository Async Extension Methods
Using IRepository Async Extension Methods has no such a problem. The examples below are pretty fine:
````csharp
var countAll = await _personRepository
.CountAsync();
var count = await _personRepository
.CountAsync(x => x.Name.StartsWith("A"));
var book1984 = await _bookRepository
.FirstOrDefaultAsync(x => x.Name == "John");
````
See the [repository documentation](https://docs.abp.io/en/abp/4.2/Repositories#iqueryable-async-operations) to understand the relation between `IQueryable` and asynchronous operations.
## .NET Package Upgrades
ABP uses the latest 5.0.* .NET packages. If your application is using 5.0.0 packages, you may get an error on build. We recommend to depend on the .NET packages like `5.0.*` in the `.csproj` files to use the latest patch versions.
Example:
````xml
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.*" />
````
## Blazorise Library Upgrade
If you are upgrading to 4.2, you also need also upgrade the following packages in your Blazor application;
* `Blazorise.Bootstrap` to `0.9.3-preview6`
* `Blazorise.Icons.FontAwesome` to `0.9.3-preview6`

@ -1,5 +1,6 @@
# ABP Framework Migration Guides
* [3.3.x to 4.0 Migration Guide](Abp-4_0.md)
* [2.9.x to 3.0 Migration Guide](../UI/Angular/Migration-Guide-v3.md)
* [4.x to 4.2](Abp-4_2.md)
* [3.3.x to 4.0](Abp-4_0.md)
* [2.9.x to 3.0](../UI/Angular/Migration-Guide-v3.md)

@ -47,7 +47,7 @@ The database connection string is located in `appsettings.json` of your `Acme.My
```json
{
"ConnectionStrings": {
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True"
}
}
```

@ -2,7 +2,7 @@
ABP is a **modular application framework** which consists of dozens of **NuGet & NPM packages**. It also provides a complete infrastructure to build your own application modules which may have entities, services, database integration, APIs, UI components and so on.
There are **two types of modules.** They don't have any structural difference but categorized by functionality and purpose:
There are **two types of modules.** They don't have any structural difference but are categorized by functionality and purpose:
* [**Framework modules**](https://github.com/abpframework/abp/tree/master/framework/src): These are **core modules of the framework** like caching, emailing, theming, security, serialization, validation, EF Core integration, MongoDB integration... etc. They do not have application/business functionalities but makes your daily development easier by providing common infrastructure, integration and abstractions.
* [**Application modules**](https://github.com/abpframework/abp/tree/master/modules): These modules implement specific application/business functionalities like blogging, document management, identity management, tenant management... etc. They generally have their own entities, services, APIs and UI components.

@ -75,7 +75,7 @@ Setting values are cached using the [distributed cache](../Caching.md) system. A
## Setting Management Providers
Setting Management module is extensible, just like the [setting system](../Settings.md). You can extend it by defining setting management providers. There are 5 pre-built setting management providers registered by the order below:
Setting Management module is extensible, just like the [setting system](../Settings.md). You can extend it by defining setting management providers. There are 5 pre-built setting management providers registered it the following order:
* `DefaultValueSettingManagementProvider`: Gets the value from the default value of the setting definition. It can not set the default value since default values are hard-coded on the setting definition.
* `ConfigurationSettingManagementProvider`: Gets the value from the [IConfiguration service](../Configuration.md). It can not set the configuration value because it is not possible to change the configuration values on runtime.

@ -149,7 +149,7 @@ public class Book : AggregateRoot<Guid>
}
```
(`BookType` is a simple enum here) And you want to create a new `Book` entity in a [domain service](Domain-Services.md):
(`BookType` is a simple `enum` here) And you want to create a new `Book` entity in a [domain service](Domain-Services.md):
```csharp
public class BookManager : DomainService
@ -215,7 +215,8 @@ public class BookRepository :
BookType type,
CancellationToken cancellationToken = default(CancellationToken))
{
await Collection.DeleteManyAsync(
var collection = await GetCollectionAsync(cancellationToken);
await collection.DeleteManyAsync(
Builders<Book>.Filter.Eq(b => b.Type, type),
cancellationToken
);
@ -253,7 +254,7 @@ public async override Task DeleteAsync(
### Access to the MongoDB API
In most cases, you want to hide MongoDB APIs behind a repository (this is the main purpose of the repository). However, if you want to access the MongoDB API over the repository, you can use `GetDatabase()` or `GetCollection()` extension methods. Example:
In most cases, you want to hide MongoDB APIs behind a repository (this is the main purpose of the repository). However, if you want to access the MongoDB API over the repository, you can use `GetDatabaseAsync()` or `GetCollectionAsync()` extension methods. Example:
```csharp
public class BookService
@ -265,10 +266,10 @@ public class BookService
_bookRepository = bookRepository;
}
public void Foo()
public async Task FooAsync()
{
IMongoDatabase database = _bookRepository.GetDatabase();
IMongoCollection<Book> books = _bookRepository.GetCollection();
IMongoDatabase database = await _bookRepository.GetDatabaseAsync();
IMongoCollection<Book> books = await _bookRepository.GetCollectionAsync();
}
}
```
@ -382,3 +383,53 @@ context.Services.AddMongoDbContext<OtherMongoDbContext>(options =>
```
In this example, `OtherMongoDbContext` implements `IBookStoreMongoDbContext`. This feature allows you to have multiple MongoDbContext (one per module) on development, but single MongoDbContext (implements all interfaces of all MongoDbContexts) on runtime.
### Customize Bulk Operations
If you have better logic or using an external library for bulk operations, you can override the logic via implementing `IMongoDbBulkOperationProvider`.
- You may use example template below:
```csharp
public class MyCustomMongoDbBulkOperationProvider
: IMongoDbBulkOperationProvider, ITransientDependency
{
public async Task DeleteManyAsync<TEntity>(
IMongoDbRepository<TEntity> repository,
IEnumerable<TEntity> entities,
IClientSessionHandle sessionHandle,
bool autoSave,
CancellationToken cancellationToken)
where TEntity : class, IEntity
{
// Your logic here.
}
public async Task InsertManyAsync<TEntity>(
IMongoDbRepository<TEntity> repository,
IEnumerable<TEntity> entities,
IClientSessionHandle sessionHandle,
bool autoSave,
CancellationToken cancellationToken)
where TEntity : class, IEntity
{
// Your logic here.
}
public async Task UpdateManyAsync<TEntity>(
IMongoDbRepository<TEntity> repository,
IEnumerable<TEntity> entities,
IClientSessionHandle sessionHandle,
bool autoSave,
CancellationToken cancellationToken)
where TEntity : class, IEntity
{
// Your logic here.
}
}
```
## See Also
* [Entities](Entities.md)
* [Repositories](Repositories.md)

@ -13,79 +13,178 @@ ABP can provide a **default generic repository** for each aggregate root or enti
**Example usage of a default generic repository:**
````C#
public class PersonAppService : ApplicationService
{
private readonly IRepository<Person, Guid> _personRepository;
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
public PersonAppService(IRepository<Person, Guid> personRepository)
namespace Demo
{
public class PersonAppService : ApplicationService
{
_personRepository = personRepository;
}
private readonly IRepository<Person, Guid> _personRepository;
public async Task Create(CreatePersonDto input)
{
var person = new Person { Name = input.Name, Age = input.Age };
public PersonAppService(IRepository<Person, Guid> personRepository)
{
_personRepository = personRepository;
}
await _personRepository.InsertAsync(person);
}
public async Task CreateAsync(CreatePersonDto input)
{
var person = new Person(input.Name);
public List<PersonDto> GetList(string nameFilter)
{
var people = _personRepository
.Where(p => p.Name.Contains(nameFilter))
.ToList();
await _personRepository.InsertAsync(person);
}
return people
.Select(p => new PersonDto {Id = p.Id, Name = p.Name, Age = p.Age})
.ToList();
public async Task<int> GetCountAsync(string filter)
{
return await _personRepository.CountAsync(p => p.Name.Contains(filter));
}
}
}
````
> See the "*IQueryable & Async Operations*" section below to understand how you can use **async extension methods**, like `ToListAsync()` (which is strongly suggested) instead of `ToList()`.
In this example;
* `PersonAppService` simply injects `IRepository<Person, Guid>` in it's constructor.
* `Create` method uses `InsertAsync` to save a newly created entity.
* `GetList` method uses the standard LINQ `Where` and `ToList` methods to filter and get a list of people from the data source.
* `CreateAsync` method uses `InsertAsync` to save the new entity.
* `GetCountAsync` method gets a filtered count of all people in the database.
> The example above uses hand-made mapping between [entities](Entities.md) and [DTO](Data-Transfer-Objects.md)s. See [object to object mapping document](Object-To-Object-Mapping.md) for an automatic way of mapping.
### Standard Repository Methods
Generic Repositories provides some standard CRUD features out of the box:
* Provides `Insert` method to save a new entity.
* `GetAsync`: Returns a single entity by its `Id` or a predicate (lambda expression).
* Throws `EntityNotFoundException` if the requested entity was not found.
* Throws `InvalidOperationException` if there are multiple entities with given predicate.
* `FindAsync`: Returns a single entity by its `Id` or a predicate (lambda expression).
* Returns `null` if the requested entity was not found.
* Throws `InvalidOperationException` if there are multiple entities with given predicate.
* `InsertAsync`: Inserts a new entity to the database.
* `UpdateAsync`: Updates an existing entity in the database.
* `DeleteAsync`: Deletes the given entity from database.
* This method has an overload that takes a predicate (lambda expression) to delete multiple entities satisfies the given condition.
* `GetListAsync`: Returns the list of all entities in the database.
* `GetPagedListAsync`: Returns a limited list of entities. Gets `skipCount`, `maxResultCount` and `sorting` parameters.
* `GetCountAsync`: Gets count of all entities in the database.
There are overloads of these methods.
* Provides `Update` and `Delete` methods to update or delete an entity by entity object or it's id.
* Provides `Delete` method to delete multiple entities by a filter.
* Implements `IQueryable<TEntity>`, so you can use LINQ and extension methods like `FirstOrDefault`, `Where`, `OrderBy`, `ToList` and so on...
### Basic Repositories
### Querying / LINQ over the Repositories
Standard `IRepository<TEntity, TKey>` interface extends standard `IQueryable<TEntity>` and you can freely query using standard LINQ methods. However, some ORM providers or database systems may not support standard `IQueryable` interface.
Repositories provide the `GetQueryableAsync()` method that returns an `IQueryable<TEntity>` object. You can use this object to perform LINQ queries on the entities in the database.
ABP provides `IBasicRepository<TEntity, TPrimaryKey>` and `IBasicRepository<TEntity>` interfaces to support such scenarios. You can extend these interfaces (and optionally derive from `BasicRepositoryBase`) to create custom repositories for your entities.
**Example: Use LINQ with the repositories**
Depending on `IBasicRepository` but not depending on `IRepository` has an advantage to make possible to work with all data sources even if they don't support `IQueryable`. But major vendors, like Entity Framework, NHibernate or MongoDb already support `IQueryable`.
````csharp
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
So, working with `IRepository` is the **suggested** way for typical applications. But reusable module developers may consider `IBasicRepository` to support a wider range of data sources.
namespace Demo
{
public class PersonAppService : ApplicationService
{
private readonly IRepository<Person, Guid> _personRepository;
### Read Only Repositories
public PersonAppService(IRepository<Person, Guid> personRepository)
{
_personRepository = personRepository;
}
There are also `IReadOnlyRepository<TEntity, TKey>` and `IReadOnlyBasicRepository<Tentity, TKey>` interfaces for who only want to depend on querying capabilities of the repositories.
public async Task<List<PersonDto>> GetListAsync(string filter)
{
//Obtain the IQueryable<Person>
IQueryable<Person> queryable = await _personRepository.GetQueryableAsync();
### Generic Repository without a Primary Key
//Create a query
var query = from person in queryable
where person.Name == filter
orderby person.Name
select person;
If your entity does not have an Id primary key (it may have a composite primary key for instance) then you cannot use the `IRepository<TEntity, TKey>` (or basic/readonly versions) defined above. In that case, you can inject and use `IRepository<TEntity>` for your entity.
//Execute the query to get list of people
var people = query.ToList();
> `IRepository<TEntity>` has a few missing methods those normally works with the `Id` property of an entity. Because of the entity has no `Id` property in that case, these methods are not available. One example is the `Get` method that gets an id and returns the entity with given id. However, you can still use `IQueryable<TEntity>` features to query entities by standard LINQ methods.
//Convert to DTO and return to the client
return people.Select(p => new PersonDto {Name = p.Name}).ToList();
}
}
}
````
You could also use the LINQ extension methods:
````csharp
public async Task<List<PersonDto>> GetListAsync(string filter)
{
//Obtain the IQueryable<Person>
IQueryable<Person> queryable = await _personRepository.GetQueryableAsync();
//Execute a query
var people = queryable
.Where(p => p.Name.Contains(filter))
.OrderBy(p => p.Name)
.ToList();
//Convert to DTO and return to the client
return people.Select(p => new PersonDto {Name = p.Name}).ToList();
}
````
Any standard LINQ method can be used over the `IQueryable` returned from the repository.
> This sample uses `ToList()` method, but it is **strongly suggested to use the asynchronous methods** to perform database queries, like `ToListAsync()` for this example.
>
> See the **IQueryable & Async Operations** section to learn how you can do it.
### Bulk Operations
There are some methods to perform bulk operations in the database;
* `InsertManyAsync`
* `UpdateManyAsync`
* `DeleteManyAsync`
These methods work with multiple entities and can take advantage of bulk operations if supported by the underlying database provider.
> Optimistic concurrency control may not be possible when you use `UpdateManyAsync` and `DeleteManyAsync` methods.
### Soft / Hard Delete
`DeleteAsync` method of the repository doesn't delete the entity if the entity is a **soft-delete** entity (that implements `ISoftDelete`). Soft-delete entities are marked as "deleted" in the database. Data Filter system ensures that the soft deleted entities are not retrieved from database normally.
If your entity is a soft-delete entity, you can use the `HardDeleteAsync` method to really delete the entity from database in case of you need it.
If your entity is a soft-delete entity, you can use the `HardDeleteAsync` method to physically delete the entity from database in case of you need it.
See the [Data Filtering](Data-Filtering.md) documentation for more about soft-delete.
> See the [Data Filtering](Data-Filtering.md) documentation for more about soft-delete.
## Other Generic Repository Types
Standard `IRepository<TEntity, TKey>` interface exposes the standard `IQueryable<TEntity>` and you can freely query using the standard LINQ methods. This is fine for most of the applications. However, some ORM providers or database systems may not support standard `IQueryable` interface. If you want to use such providers, you can't rely on the `IQueryable`.
### Basic Repositories
ABP provides `IBasicRepository<TEntity, TPrimaryKey>` and `IBasicRepository<TEntity>` interfaces to support such scenarios. You can extend these interfaces (and optionally derive from `BasicRepositoryBase`) to create custom repositories for your entities.
Depending on `IBasicRepository` but not depending on `IRepository` has an advantage to make possible to work with all data sources even if they don't support `IQueryable`.
Major vendors, like Entity Framework, NHibernate or MongoDB already support `IQueryable`. So, working with `IRepository` is the **suggested** way for typical applications. But reusable module developers may consider `IBasicRepository` to support a wider range of data sources.
### Read Only Repositories
There are also `IReadOnlyRepository<TEntity, TKey>` and `IReadOnlyBasicRepository<Tentity, TKey>` interfaces for who only want to depend on querying capabilities of the repositories.
### Generic Repository without a Primary Key
If your entity does not have an Id primary key (it may have a composite primary key for instance) then you cannot use the `IRepository<TEntity, TKey>` (or basic/readonly versions) defined above. In that case, you can inject and use `IRepository<TEntity>` for your entity.
> `IRepository<TEntity>` has a few missing methods those normally works with the `Id` property of an entity. Because of the entity has no `Id` property in that case, these methods are not available. One example is the `Get` method that gets an id and returns the entity with given id. However, you can still use `IQueryable<TEntity>` features to query entities by standard LINQ methods.
## Custom Repositories
@ -123,7 +222,8 @@ public class PersonRepository : EfCoreRepository<MyDbContext, Person, Guid>, IPe
public async Task<Person> FindByNameAsync(string name)
{
return await DbContext.Set<Person>()
var dbSet = await GetDbSetAsync();
return await dbSet.Set<Person>()
.Where(p => p.Name == name)
.FirstOrDefaultAsync();
}
@ -136,12 +236,13 @@ You can directly access the data access provider (`DbContext` in this case) to p
## IQueryable & Async Operations
`IRepository` inherits from `IQueryable`, that means you can **directly use LINQ extension methods** on it, as shown in the example of the "*Generic Repositories*" section above.
`IRepository` provides `GetQueryableAsync()` to obtain an `IQueryable`, that means you can **directly use LINQ extension methods** on it, as shown in the example of the "*Querying / LINQ over the Repositories*" section above.
**Example: Using the `Where(...)` and the `ToList()` extension methods**
````csharp
var people = _personRepository
var queryable = await _personRepository.GetQueryableAsync();
var people = queryable
.Where(p => p.Name.Contains(nameFilter))
.ToList();
````
@ -170,7 +271,8 @@ When you add the NuGet package to your project, you can take full power of the E
**Example: Directly using the `ToListAsync()` after adding the EF Core package**
````csharp
var people = _personRepository
var queryable = await _personRepository.GetQueryableAsync();
var people = queryable
.Where(p => p.Name.Contains(nameFilter))
.ToListAsync();
````
@ -186,7 +288,8 @@ If you are using [MongoDB](MongoDB.md), you need to add the [Volo.Abp.MongoDB](h
**Example: Cast `IQueryable<T>` to `IMongoQueryable<T>` and use `ToListAsync()`**
````csharp
var people = ((IMongoQueryable<Person>)_personRepository
var queryable = await _personRepository.GetQueryableAsync();
var people = ((IMongoQueryable<Person>) queryable
.Where(p => p.Name.Contains(nameFilter)))
.ToListAsync();
````
@ -213,10 +316,11 @@ The standard LINQ extension methods are supported: *AllAsync, AnyAsync, AverageA
This approach still **has a limitation**. You need to call the extension method directly on the repository object. For example, the below usage is **not supported**:
```csharp
var count = await _bookRepository.Where(x => x.Name.Contains("A")).CountAsync();
var queryable = await _bookRepository.GetQueryableAsync();
var count = await queryable.Where(x => x.Name.Contains("A")).CountAsync();
```
This is because the object returned from the `Where` method is not a repository object, it is a standard `IQueryable` interface. See the other options for such cases.
This is because the `CountAsync()` method in this example is called on a `IQueryable` interface, not on the repository object. See the other options for such cases.
This method is suggested **wherever possible**.
@ -253,8 +357,11 @@ namespace AbpDemo
public async Task<ListResultDto<ProductDto>> GetListAsync(string name)
{
//Obtain the IQueryable<T>
var queryable = await _productRepository.GetQueryableAsync();
//Create the query
var query = _productRepository
var query = queryable
.Where(p => p.Name.Contains(name))
.OrderBy(p => p.Name);

@ -17,6 +17,9 @@ A simple CRUD application to show basic principles of developing an application
* **Book Store: Razor Pages UI & Entity Framework Core**
* [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC&DB=EF)
* [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
* **Book Store: Blazor UI & Entity Framework Core**
* [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=Blazor&DB=EF)
* [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
* **Book Store: Angular UI & MongoDB**
* [Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG&DB=Mongo)
* [Source code](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)

@ -19,7 +19,7 @@ This sample aims to demonstrate a simple yet complete microservice solution;
* Has a **console application** to show the simplest way of using a service by authenticating.
* Uses [Redis](https://redis.io/) for **distributed caching**.
* Uses [RabbitMQ](https://www.rabbitmq.com/) for service-to-service **messaging**.
* Uses [Docker](https://www.docker.com/) & [Kubernates](https://kubernetes.io/) to **deploy** & run all services and applications.
* Uses [Docker](https://www.docker.com/) & [Kubernetes](https://kubernetes.io/) to **deploy** & run all services and applications.
* Uses [Elasticsearch](https://www.elastic.co/products/elasticsearch) & [Kibana](https://www.elastic.co/products/kibana) to store and visualize the logs (written using [Serilog](https://serilog.net/)).
The diagram below shows the system:
@ -842,7 +842,7 @@ It has a dedicated MongoDB database (MsDemo_Blogging) to store blog and posts. I
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true",
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True",
"Blogging": "mongodb://localhost/MsDemo_Blogging"
}
````
@ -968,8 +968,8 @@ There are two connection strings in the `appsettings.json` file:
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true",
"ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True",
"ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True"
}
````
@ -1416,4 +1416,4 @@ An Audit Log record has a `CorrelationId` property that can be used to track a r
### Multi-Tenancy
The solution has been configured to provide a [multi-tenant](../Multi-Tenancy.md) system, where each tenant can have their isolated users, roles, permissions and other data.
The solution has been configured to provide a [multi-tenant](../Multi-Tenancy.md) system, where each tenant can have their isolated users, roles, permissions and other data.

@ -122,7 +122,8 @@ namespace MyProject
public async Task<List<Customer>> GetCustomersCanBuyAlcohol()
{
var query = _customerRepository.Where(
var queryable = await _customerRepository.GetQueryableAsync();
var query = queryable.Where(
new Age18PlusCustomerSpecification().ToExpression()
);
@ -137,7 +138,8 @@ namespace MyProject
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(
var queryable = await _customerRepository.GetQueryableAsync();
var query = queryable.Where(
new Age18PlusCustomerSpecification()
);
````

@ -364,10 +364,11 @@ namespace Acme.BookStore.Books
public override async Task<BookDto> GetAsync(Guid id)
{
await CheckGetPolicyAsync();
//Get the IQueryable<Book> from the repository
var queryable = await Repository.GetQueryableAsync();
//Prepare a query to join books and authors
var query = from book in Repository
var query = from book in queryable
join author in _authorRepository on book.AuthorId equals author.Id
where book.Id == id
select new { book, author };
@ -384,17 +385,24 @@ namespace Acme.BookStore.Books
return bookDto;
}
public override async Task<PagedResultDto<BookDto>> GetListAsync(
PagedAndSortedResultRequestDto input)
public override async Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
await CheckGetListPolicyAsync();
//Set a default sorting, if not provided
if (input.Sorting.IsNullOrWhiteSpace())
{
input.Sorting = nameof(Book.Name);
}
//Get the IQueryable<Book> from the repository
var queryable = await Repository.GetQueryableAsync();
//Prepare a query to join books and authors
var query = from book in Repository
var query = from book in queryable
join author in _authorRepository on book.AuthorId equals author.Id
orderby input.Sorting
orderby input.Sorting //TODO: Can not sort like that!
select new {book, author};
//Paging
query = query
.Skip(input.SkipCount)
.Take(input.MaxResultCount);
@ -437,7 +445,7 @@ Let's see the changes we've done:
* Injected `IAuthorRepository` to query from the authors.
* Overrode the `GetAsync` method of the base `CrudAppService`, which returns a single `BookDto` object with the given `id`.
* Used a simple LINQ expression to join books and authors and query them together for the given book id.
* Used `AsyncExecuter.FirstOrDefaultAsync(...)` to execute the query and get a result. `AsyncExecuter` was previously used in the `AuthorAppService`. Check the [repository documentation](../Repositories.md) to understand why we've used it.
* Used `AsyncExecuter.FirstOrDefaultAsync(...)` to execute the query and get a result. It is a way to use asynchronous LINQ extensions without depending on the database provider API. Check the [repository documentation](../Repositories.md) to understand why we've used it.
* Throws an `EntityNotFoundException` which results an `HTTP 404` (not found) result if requested book was not present in the database.
* Finally, created a `BookDto` object using the `ObjectMapper`, then assigning the `AuthorName` manually.
* Overrode the `GetListAsync` method of the base `CrudAppService`, which returns a list of books. The logic is similar to the previous method, so you can easily understand the code.
@ -487,8 +495,6 @@ namespace Acme.BookStore.Books
public async override Task<BookDto> GetAsync(Guid id)
{
await CheckGetPolicyAsync();
var book = await Repository.GetAsync(id);
var bookDto = ObjectMapper.Map<Book, BookDto>(book);
@ -501,17 +507,18 @@ namespace Acme.BookStore.Books
public async override Task<PagedResultDto<BookDto>>
GetListAsync(PagedAndSortedResultRequestDto input)
{
await CheckGetListPolicyAsync();
//Set a default sorting, if not provided
if (input.Sorting.IsNullOrWhiteSpace())
{
input.Sorting = nameof(Book.Name);
}
//Get the IQueryable<Book> from the repository
var queryable = await Repository.GetQueryableAsync();
//Get the books
var books = await AsyncExecuter.ToListAsync(
Repository
queryable
.OrderBy(input.Sorting)
.Skip(input.SkipCount)
.Take(input.MaxResultCount)
@ -553,8 +560,10 @@ namespace Acme.BookStore.Books
.Distinct()
.ToArray();
var queryable = await _authorRepository.GetQueryableAsync();
var authors = await AsyncExecuter.ToListAsync(
_authorRepository.Where(a => authorIds.Contains(a.Id))
queryable.Where(a => authorIds.Contains(a.Id))
);
return authors.ToDictionary(x => x.Id, x => x);

@ -127,7 +127,8 @@ namespace Acme.BookStore.Authors
public async Task<Author> FindByNameAsync(string name)
{
return await DbSet.FirstOrDefaultAsync(author => author.Name == name);
var dbSet = await GetDbSetAsync();
return await dbSet.FirstOrDefaultAsync(author => author.Name == name);
}
public async Task<List<Author>> GetListAsync(
@ -136,7 +137,8 @@ namespace Acme.BookStore.Authors
string sorting,
string filter = null)
{
return await DbSet
var dbSet = await GetDbSetAsync();
return await dbSet
.WhereIf(
!filter.IsNullOrWhiteSpace(),
author => author.Name.Contains(filter)
@ -186,8 +188,8 @@ namespace Acme.BookStore.Authors
public async Task<Author> FindByNameAsync(string name)
{
return await GetMongoQueryable()
.FirstOrDefaultAsync(author => author.Name == name);
var queryable = await GetMongoQueryableAsync();
return await queryable.FirstOrDefaultAsync(author => author.Name == name);
}
public async Task<List<Author>> GetListAsync(
@ -196,7 +198,8 @@ namespace Acme.BookStore.Authors
string sorting,
string filter = null)
{
return await GetMongoQueryable()
var queryable = await GetMongoQueryableAsync();
return await queryable
.WhereIf<Author, IMongoQueryable<Author>>(
!filter.IsNullOrWhiteSpace(),
author => author.Name.Contains(filter)

@ -172,6 +172,7 @@ using System.Threading.Tasks;
using Acme.BookStore.Permissions;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Authors
{
@ -230,12 +231,10 @@ public async Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input
input.Filter
);
var totalCount = await AsyncExecuter.CountAsync(
_authorRepository.WhereIf(
!input.Filter.IsNullOrWhiteSpace(),
author => author.Name.Contains(input.Filter)
)
);
var totalCount = input.Filter == null
? await _authorRepository.CountAsync()
: await _authorRepository.CountAsync(
author => author.Name.Contains(input.Filter));
return new PagedResultDto<AuthorDto>(
totalCount,
@ -246,7 +245,7 @@ public async Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input
* Default sorting is "by author name" which is done in the beginning of the method in case of it wasn't sent by the client.
* Used the `IAuthorRepository.GetListAsync` to get a paged, sorted and filtered list of authors from the database. We had implemented it in the previous part of this tutorial. Again, it actually was not needed to create such a method since we could directly query over the repository, but wanted to demonstrate how to create custom repository methods.
* Directly queried from the `AuthorRepository` while getting the count of the authors. We preferred to use the `AsyncExecuter` service which allows us to perform async queries without depending on the EF Core. However, you could depend on the EF Core package and directly use the `_authorRepository.WhereIf(...).ToListAsync()` method. See the [repository document](../Repositories.md) to read the alternative approaches and the discussion.
* Directly queried from the `AuthorRepository` while getting the count of the authors. If a filter is sent, then we are using it to filter entities while getting the count.
* Finally, returning a paged result by mapping the list of `Author`s to a list of `AuthorDto`s.
### CreateAsync

@ -0,0 +1,64 @@
# Page Alerts
A page alert is useful for displaying an important message to the user. The ABP Framework provides an easy way to show the following alert to the user.
![angular-page-alert-example](./images/page-alert-warning-example.png)
You can simply import `PageAlertService` from `@abp/ng.theme.shared` and utilize it as follows:
```js
import { PageAlertService } from '@abp/ng.theme.shared';
@Component({
// ...
})
export class MyComponent {
constructor(private service: PageAlertService) {}
showWarning() {
this.service.show({
type: 'warning',
message:
'We will have a service interruption between 02:00 AM and 04:00 AM at October 23, 2023!',
title: 'Service Interruption',
});
}
}
```
## `SHOW`
The method `show` accepts a single object that is type of `PageAlert`
```js
export interface PageAlert {
type: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark';
message: string;
title?: string;
dismissible?: boolean;
messageLocalizationParams?: string[];
titleLocalizationParams?: string[];
}
```
* `type` (Required): Defines what type of alert will be shown
* `message` (Required): The message who will be shown, also works with localization as well.
* `title` (Optional): The title of the message. If it is not provided, the title will be hidden.
* `dismissible` (Optional): Default is `true`. If enabled, a button on the top right corner will be shown to the users so that they can dismiss the message.
* `messageLocalizationParams` and `titleLocalizationParams` (Optional): If the message and/or the title is a key for localization service and contains some parameters, these fields could be used to pass those parameters.
### An example with Localization
```typescript
this.service.show({
type: 'danger',
message: 'AbpAccount::PagerInfo{0}{1}{2}',
messageLocalizationParams: ['10', '20', '30'],
title: 'AbpAccount::EntityNotFoundErrorMessage',
titleLocalizationParams: ['Test', 'id123'],
});
```
![angular-page-alert-with-params-example](./images/page-alert-with-params-example.png)

@ -1,3 +1,380 @@
# Angular UI: Testing
# Unit Testing Angular UI
TODO
ABP Angular UI is tested like any other Angular application. So, [the guide here](https://angular.io/guide/testing) applies to ABP too. That said, we would like to point out some **unit testing topics specific to ABP Angular applications**.
## Setup
In Angular, unit tests use [Karma](https://karma-runner.github.io/) and [Jasmine](https://jasmine.github.io) by default. Although we like Jest more, we chose not to deviate from these defaults, so **the application template you download will have Karma and Jasmine preconfigured**. You can find the Karma configuration inside the _karma.conf.js_ file in the root folder. You don't have to do anything. Adding a spec file and running `npm test` will work.
## Basics
An over-simplified spec file looks like this:
```js
import { CoreTestingModule } from "@abp/ng.core/testing";
import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing";
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { NgxValidateCoreModule } from "@ngx-validate/core";
import { MyComponent } from "./my.component";
describe("MyComponent", () => {
let fixture: ComponentFixture<MyComponent>;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [MyComponent],
imports: [
CoreTestingModule.withConfig(),
ThemeSharedTestingModule.withConfig(),
ThemeBasicTestingModule.withConfig(),
NgxValidateCoreModule,
],
providers: [
/* mock providers here */
],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
});
it("should be initiated", () => {
expect(fixture.componentInstance).toBeTruthy();
});
});
```
If you take a look at the imports, you will notice that we have prepared some testing modules to replace built-in ABP modules. This is necessary for providing mocks for some features which otherwise would break your tests. Please remember to **use testing modules** and **call their `withConfig` static method**.
## Tips
### Angular Testing Library
Although you can test your code with Angular TestBed, you may find [Angular Testing Library](https://testing-library.com/docs/angular-testing-library/intro) a good alternative.
The simple example above can be written with Angular Testing Library as follows:
```js
import { CoreTestingModule } from "@abp/ng.core/testing";
import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing";
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
import { ComponentFixture } from "@angular/core/testing";
import { NgxValidateCoreModule } from "@ngx-validate/core";
import { render } from "@testing-library/angular";
import { MyComponent } from "./my.component";
describe("MyComponent", () => {
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
const result = await render(MyComponent, {
imports: [
CoreTestingModule.withConfig(),
ThemeSharedTestingModule.withConfig(),
ThemeBasicTestingModule.withConfig(),
NgxValidateCoreModule,
],
providers: [
/* mock providers here */
],
});
fixture = result.fixture;
});
it("should be initiated", () => {
expect(fixture.componentInstance).toBeTruthy();
});
});
```
Very similar, as you can see. The real difference kicks in when we use queries and fire events.
```js
// other imports
import { getByLabelText, screen } from "@testing-library/angular";
import userEvent from "@testing-library/user-event";
describe("MyComponent", () => {
beforeEach(/* removed for sake of brevity */);
it("should display advanced filters", () => {
const filters = screen.getByTestId("author-filters");
const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement;
expect(nameInput.offsetWidth).toBe(0);
const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i });
userEvent.click(advancedFiltersBtn);
expect(nameInput.offsetWidth).toBeGreaterThan(0);
userEvent.type(nameInput, "fooo{backspace}");
expect(nameInput.value).toBe("foo");
});
});
```
The **queries in Angular Testing Library follow practices for maintainable tests**, the user event package provides a **human-like interaction** with the DOM, and the library in general has **a clear API** that simplifies component testing. Please find some useful links below:
- [Queries](https://testing-library.com/docs/dom-testing-library/api-queries)
- [User Event](https://testing-library.com/docs/ecosystem-user-event)
- [Examples](https://github.com/testing-library/angular-testing-library/tree/master/apps/example-app/app/examples)
### Clearing DOM After Each Spec
One thing to remember is that Karma runs tests in real browser instances. That means, you will be able to see the result of your test code, but also have problems with components attached to the document body which may not get cleared after each test, even when you configure Karma to do so.
We have prepared a simple function with which you can clear any leftover DOM elements after each test.
```js
// other imports
import { clearPage } from "@abp/ng.core/testing";
describe("MyComponent", () => {
let fixture: ComponentFixture<MyComponent>;
afterEach(() => clearPage(fixture));
beforeEach(async () => {
const result = await render(MyComponent, {
/* removed for sake of brevity */
});
fixture = result.fixture;
});
// specs here
});
```
Please make sure you use it because Karma will fail to remove dialogs otherwise and you will have multiple copies of modals, confirmation boxes, and alike.
### Waiting
Some components, modals, in particular, work off-detection-cycle. In other words, you cannot reach DOM elements inserted by these components immediately after opening them. Similarly, inserted elements are not immediately destroyed upon closing them.
For this purpose, we have prepared a `wait` function.
```js
// other imports
import { wait } from "@abp/ng.core/testing";
describe("MyComponent", () => {
beforeEach(/* removed for sake of brevity */);
it("should open a modal", async () => {
const openModalBtn = screen.getByRole("button", { name: "Open Modal" });
userEvent.click(openModalBtn);
await wait(fixture);
const modal = screen.getByRole("dialog");
expect(modal).toBeTruthy();
/* wait again after closing the modal */
});
});
```
The `wait` function takes a second parameter, i.e. timeout (default: `0`). Try not to use it though. Using a timeout bigger than `0` is usually a signal that something is not quite right.
## Testing Example
Here is an example test suite. It doesn't cover all, but gives quite a good idea about what the testing experience will be like.
```js
import { clearPage, CoreTestingModule, wait } from "@abp/ng.core/testing";
import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing";
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
import { ComponentFixture } from "@angular/core/testing";
import {
NgbCollapseModule,
NgbDatepickerModule,
NgbDropdownModule,
} from "@ng-bootstrap/ng-bootstrap";
import { NgxValidateCoreModule } from "@ngx-validate/core";
import { CountryService } from "@proxy/countries";
import {
findByText,
getByLabelText,
getByRole,
getByText,
queryByRole,
render,
screen,
} from "@testing-library/angular";
import userEvent from "@testing-library/user-event";
import { BehaviorSubject, of } from "rxjs";
import { CountryComponent } from "./country.component";
const list$ = new BehaviorSubject({
items: [{ id: "ID_US", name: "United States of America" }],
totalCount: 1,
});
describe("Country", () => {
let fixture: ComponentFixture<CountryComponent>;
afterEach(() => clearPage(fixture));
beforeEach(async () => {
const result = await render(CountryComponent, {
imports: [
CoreTestingModule.withConfig(),
ThemeSharedTestingModule.withConfig(),
ThemeBasicTestingModule.withConfig(),
NgxValidateCoreModule,
NgbCollapseModule,
NgbDatepickerModule,
NgbDropdownModule,
],
providers: [
{
provide: CountryService,
useValue: {
getList: () => list$,
},
},
],
});
fixture = result.fixture;
});
it("should display advanced filters", () => {
const filters = screen.getByTestId("country-filters");
const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement;
expect(nameInput.offsetWidth).toBe(0);
const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i });
userEvent.click(advancedFiltersBtn);
expect(nameInput.offsetWidth).toBeGreaterThan(0);
userEvent.type(nameInput, "fooo{backspace}");
expect(nameInput.value).toBe("foo");
userEvent.click(advancedFiltersBtn);
expect(nameInput.offsetWidth).toBe(0);
});
it("should have a heading", () => {
const heading = screen.getByRole("heading", { name: "Countries" });
expect(heading).toBeTruthy();
});
it("should render list in table", async () => {
const table = await screen.findByTestId("country-table");
const name = getByText(table, "United States of America");
expect(name).toBeTruthy();
});
it("should display edit modal", async () => {
const actionsBtn = screen.queryByRole("button", { name: /actions/i });
userEvent.click(actionsBtn);
const editBtn = screen.getByRole("button", { name: /edit/i });
userEvent.click(editBtn);
await wait(fixture);
const modal = screen.getByRole("dialog");
const modalHeading = queryByRole(modal, "heading", { name: /edit/i });
expect(modalHeading).toBeTruthy();
const closeBtn = getByText(modal, "×");
userEvent.click(closeBtn);
await wait(fixture);
expect(screen.queryByRole("dialog")).toBeFalsy();
});
it("should display create modal", async () => {
const newBtn = screen.getByRole("button", { name: /new/i });
userEvent.click(newBtn);
await wait(fixture);
const modal = screen.getByRole("dialog");
const modalHeading = queryByRole(modal, "heading", { name: /new/i });
expect(modalHeading).toBeTruthy();
});
it("should validate required name field", async () => {
const newBtn = screen.getByRole("button", { name: /new/i });
userEvent.click(newBtn);
await wait(fixture);
const modal = screen.getByRole("dialog");
const nameInput = getByRole(modal, "textbox", {
name: /^name/i,
}) as HTMLInputElement;
userEvent.type(nameInput, "x");
userEvent.type(nameInput, "{backspace}");
const nameError = await findByText(modal, /required/i);
expect(nameError).toBeTruthy();
});
it("should delete a country", () => {
const getSpy = spyOn(fixture.componentInstance.list, "get");
const deleteSpy = jasmine.createSpy().and.returnValue(of(null));
fixture.componentInstance.service.delete = deleteSpy;
const actionsBtn = screen.queryByRole("button", { name: /actions/i });
userEvent.click(actionsBtn);
const deleteBtn = screen.getByRole("button", { name: /delete/i });
userEvent.click(deleteBtn);
const confirmText = screen.getByText("AreYouSure");
expect(confirmText).toBeTruthy();
const confirmBtn = screen.getByRole("button", { name: "Yes" });
userEvent.click(confirmBtn);
expect(deleteSpy).toHaveBeenCalledWith(list$.value.items[0].id);
expect(getSpy).toHaveBeenCalledTimes(1);
});
});
```
## CI Configuration
You would need a different configuration for your CI environment. To set up a new configuration for your unit tests, find the test project in _angular.json_ file and add one as seen below:
```json
// angular.json
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": { /* several options here */ },
"configurations": {
"production": {
"karmaConfig": "karma.conf.prod.js"
}
}
}
```
Now you can copy the _karma.conf.js_ as _karma.conf.prod.js_ and use any configuration you like in it. Please check [Karma configuration file document](http://karma-runner.github.io/5.2/config/configuration-file.html) for config options.
Finally, don't forget to run your CI tests with the following command:
```sh
npm test -- --prod
```
## See Also
* [ABP Community Video - Unit Testing with the Angular UI](https://community.abp.io/articles/unit-testing-with-the-angular-ui-p4l550q3)

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

@ -20,13 +20,15 @@ namespace MyProject.Web
public class MyProjectBrandingProvider : DefaultBrandingProvider
{
public override string AppName => "Book Store";
public override string LogoUrl => "logo.png";
}
}
````
The result will be like shown below:
![branding-appname](../../images/branding-appname.png)
![bookstore-added-logo](../../images/bookstore-added-logo.png)
`IBrandingProvider` has the following properties:
@ -38,8 +40,4 @@ The result will be like shown below:
## Overriding the Branding Area
The [Basic Theme](Basic-Theme.md) doesn't implement the logos. However, you can see the [UI Customization Guide](Customization-User-Interface.md) to learn how you can replace the branding area with a custom view component.
An example screenshot with an image is used in the branding area:
![bookstore-added-logo](../../images/bookstore-added-logo.png)
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.

@ -0,0 +1,57 @@
# Blazor UI: Page Progress
Page Progress is used to show a progress bar indicator on top of the page and to show to the user that currently a long running process is in the work.
By default you don't need to do anything to show the progress indicator, as all the work is done automatically by the ABP Framework internals. This means that all calls to the ABP backend (through your HTTP API) will activate page progress and show the loading indicator.
This doesn't mean that you don't have the control over it. On the contrary, if you want to show progress for your own processes, it is really easy to do. All you have to do is to use inject and use the `IUiPageProgressService`.
## Example
First, inject the `IUiPageProgressService` into your page/component.
```cs
@inject IUiPageProgressService pageProgressService
```
Next, invoke the `Go` method in `IUiPageProgressService`. It's that simple:
```cs
Task OnClick()
{
return pageProgressService.Go(null);
}
```
The previous example will show the progress with a default settings. If, for example you want to change the progress color you can override it by setting the options through the `Go` method.
```cs
Task OnClick()
{
return pageProgressService.Go(null, options =>
{
options.Type = UiPageProgressType.Warning;
});
}
```
## Breakdown
The first parameter of the `Go` needs a little explanation. In the previous example we have set it to `null` which means, once called it will show an _indeterminate_ indicator and will cycle the loading animation indefinitely, until we hide the progress. You also have the option of defining the actual percentage of the progress and the code is the same, just instead of sending it the `null` you will send it a number between `0` and `100`.
```cs
pageProgressService.Go(25)
```
### Valid values
1. `null` - show _indeterminate_ indicator
2. `>= 0` and `<= 100` - show the regular _percentage_ progress
### Hiding progress
To hide the progress just set the actual values to something other then the _Valid value_.
```cs
pageProgressService.Go(-1)
```

@ -681,6 +681,10 @@
{
"text": "Page Alerts",
"path": "UI/Blazor/Page-Alerts.md"
},
{
"text": "Page Progress",
"path": "UI/Blazor/Page-Progress.md"
}
]
},
@ -740,6 +744,10 @@
{
"text": "PWA Configuration",
"path": "UI/Angular/PWA-Configuration.md"
},
{
"text": "Unit Testing",
"path": "UI/Angular/Testing.md"
}
]
},
@ -818,6 +826,10 @@
{
"text": "Toast Overlay",
"path": "UI/Angular/Toaster-Service.md"
},
{
"text": "Page Alerts",
"path": "UI/Angular/Page-Alerts.md"
}
]
},
@ -1044,10 +1056,6 @@
{
"text": "CLI",
"path": "CLI.md"
},
{
"text": "API Documentation",
"path": "{ApiDocumentationUrl}"
}
]
},

@ -55,7 +55,7 @@ Configure<AbpBlobStoringOptions>(options =>
* **RoleSessionName** ([NotNull]string): 用来标识临时访问凭证的名称,建议使用不同的应用程序用户来区分.
* **Policy** (string): 在扮演角色的时候额外添加的权限限制. 请参见[基于RAM Policy的权限控制](https://help.aliyun.com/document_detail/100680.html).
* **DurationSeconds** (int): 设置临时访问凭证的有效期,单位是s,最小为900,最大为3600.
* **ContainerName** (string): 你可以在aliyun中指定容器名称. 如果没有指定它将使用 `BlogContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aliyun有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://help.aliyun.com/knowledge_detail/39668.html):
* **ContainerName** (string): 你可以在aliyun中指定容器名称. 如果没有指定它将使用 `BlobContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aliyun有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://help.aliyun.com/knowledge_detail/39668.html):
* 只能包含小写字母,数字和短横线(-)
* 必须以小写字母和数字开头和结尾
* Bucket名称的长度限制在**3**到**63**个字符之间

@ -59,7 +59,7 @@ Configure<AbpBlobStoringOptions>(options =>
* **Region** (string): 服务的地区名称.
* **Policy** (string): JSON格式的IAM策略.
* **DurationSeconds** (int): 设置临时访问凭证的有效期,单位是s,最小为900,最大为129600.
* **ContainerName** (string): 你可以在Aws中指定容器名称. 如果没有指定它将使用 `BlogContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aws有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html):
* **ContainerName** (string): 你可以在Aws中指定容器名称. 如果没有指定它将使用 `BlobContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aws有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html):
* Bucket名称必须介于 3 到 63 个字符之间.
* Bucket名称只能由小写字母、数字、句点 (.) 和连字符 (-) 组成.
* Bucket名称必须以字母或数字开头和结尾.

@ -40,7 +40,7 @@ Configure<AbpBlobStoringOptions>(options =>
### 选项
* **ConnectionString** (string): 连接字符串包括应用程序在运行时使用共享密钥授权访问Azure存储帐户中的数据所需的授权信息. 请参考[Azure文档](https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string).
* **ContainerName** (string): 你可以在azure中指定容器名称. 如果没有指定它将使用 `BlogContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Azure有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names):
* **ContainerName** (string): 你可以在azure中指定容器名称. 如果没有指定它将使用 `BlobContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Azure有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names):
* 容器名称必须以字母或数字开头或结尾,并且只能包含字母,数字和破折号(-)字符.
* 每个破折号(-)必须紧跟在字母或数字之后;容器名称中不允许使用连续的破折号.
* 容器名称中的所有字母都必须**小写**.

@ -43,7 +43,7 @@ Configure<AbpBlobStoringOptions>(options =>
* **EndPoint** (string): 你的Minio对象存储服务的URL, 查看文档https://docs.min.io/docs/dotnet-client-quickstart-guide.html
* **AccessKey** (string): Access key是唯一标识你的账户的用户ID,
* **SecretKey** (string): Access key是唯一标识你的账户的用户ID
* **BucketName** (string):你可以指定bucket名称,如果没有指定,将使用 `BlogContainerName` 属性定义的BLOB容器的名称(查阅[BLOB storing document](Blob-Storing.md)),MinIO完全兼容S3标准,所以有一些 **bucket命名规则**,必须符合[规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html):
* **BucketName** (string):你可以指定bucket名称,如果没有指定,将使用 `BlobContainerName` 属性定义的BLOB容器的名称(查阅[BLOB storing document](Blob-Storing.md)),MinIO完全兼容S3标准,所以有一些 **bucket命名规则**,必须符合[规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html):
* Bucket名称必须介于 3 到 63 个字符之间.
* Bucket名称只能由小写字母、数字、句点 (.) 和连字符 (-) 组成.
* Bucket名称必须以字母或数字开头和结尾.

@ -10,7 +10,7 @@ ABP框架为BLOB提供了抽象,并提供了一些可以轻松集成到的预构
* 你可以**轻松的更改**BLOB存储,而不用改变你的应用程序代码.
* 如果你想创建**可重用的应用程序模块**,无需假设BLOB的存储方式.
ABP BLOG存储系统兼容ABP框架其他功能,如[多租户](Multi-Tenancy.md).
ABP BLOB存储系统兼容ABP框架其他功能,如[多租户](Multi-Tenancy.md).
## BLOB 存储提供程序
@ -18,9 +18,9 @@ ABP框架已经有以下存储提供程序的实现;
* [File System](Blob-Storing-File-System.md):将BLOB作为标准文件存储在本地文件系统的文件夹中.
* [Database](Blob-Storing-Database.md): 将BLOB存储在数据库中.
* [Azure](Blob-Storing-Azure.md): 将BLOG存储在 [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/)中.
* [Azure](Blob-Storing-Azure.md): 将BLOB存储在 [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/)中.
* [Aliyun](Blob-Storing-Aliyun.md): 将BLOB存储在[Aliyun Storage Service](https://help.aliyun.com/product/31815.html)中.
* [Ninio](Blob-Storing-Minio.md): 将BLOB存储在[MinIO Object storage](https://min.io/)中.
* [Minio](Blob-Storing-Minio.md): 将BLOB存储在[MinIO Object storage](https://min.io/)中.
* [Aws](Blob-Storing-Aws.md): 将BLOB存储在[Amazon Simple Storage Service](https://aws.amazon.com/s3/)中.
以后会实现更多的提供程序,你可以为自己喜欢的提供程序创建[请求](https://github.com/abpframework/abp/issues/new),或者你也可以[自己实现](Blob-Storing-Custom-Provider.md)它并[贡献](Contribution/Index.md)到ABP框架.
@ -95,9 +95,9 @@ namespace AbpDemo
### 读取/获取 BLOB
* `GetAsync`: 返回给定BLOB名称可用于读取BLOB内容的 `Stream` 对象. 使用后始终要**dispose流**. 如果找不到具有给定名称的BLOB,则抛出异常.
* `GetOrNullAsync`: 与 `GetAsync` 方法相反,如果未找到给定名称的BLOG,则返回 `null`.
* `GetOrNullAsync`: 与 `GetAsync` 方法相反,如果未找到给定名称的BLOB,则返回 `null`.
* `GetAllBytesAsync`: 返回 `byte[]` 而不是 `Stream`. 如果找不到具有给定名称的BLOB,则抛出异常.
* `GetAllBytesOrNullAsync`: 与 `GetAllBytesAsync` 方法相反,如果未找到给定名称的BLOG,则返回 `null`.
* `GetAllBytesOrNullAsync`: 与 `GetAllBytesAsync` 方法相反,如果未找到给定名称的BLOB,则返回 `null`.
### 删除 BLOB
@ -300,7 +300,7 @@ Configure<AbpBlobStoringOptions>(options =>
## BLOB 存储 vs 文件管理系统
注意BLOB存储不是一个文件管理系统. 它是一个用于保存,获取和删除命名BLOG的低级别系统. 它不提供目录那样的层次结构,这是典型文件系统所期望的.
注意BLOB存储不是一个文件管理系统. 它是一个用于保存,获取和删除命名BLOB的低级别系统. 它不提供目录那样的层次结构,这是典型文件系统所期望的.
如果你想创建文件夹并在文件夹之间移动文件,为文件分配权限并在用户之间共享文件,那么你需要在BLOB存储系统上实现你自己的应用程序.

@ -103,7 +103,7 @@ abp new Acme.BookStore
* `--preview`: 使用最新的预览版本.
* `--template-source` 或者 `-ts`: 指定自定义模板源用于生成项目,可以使用本地源和网络源(例如 `D:\local-templat``https://.../my-template-file.zip`).
* `--create-solution-folder` 或者 `-csf`: 指定项目是在输出文件夹中的新文件夹中还是直接在输出文件夹中.
* `--connection-string` 或者 `-cs`: 重写所有 `appsettings.json` 文件的默认连接字符串. 默认连接字符串是 `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true`. 默认的数据库提供程序是 `SQL Server`. 如果你使用EF Core但需要更改DBMS,可以按[这里所述](Entity-Framework-Core-Other-DBMS.md)进行更改(创建解决方案之后).
* `--connection-string` 或者 `-cs`: 重写所有 `appsettings.json` 文件的默认连接字符串. 默认连接字符串是 `Server=localhost;Database=MyProjectName;Trusted_Connection=True`. 默认的数据库提供程序是 `SQL Server`. 如果你使用EF Core但需要更改DBMS,可以按[这里所述](Entity-Framework-Core-Other-DBMS.md)进行更改(创建解决方案之后).
* `--local-framework-ref --abp-path`: 使用对项目的本地引用,而不是替换为NuGet包引用.
### update

@ -1315,7 +1315,7 @@ namespace IssueTracking.Users
#### 输出DTO最佳实践
* 保持**数量较少**的输出DTO,尽可能**重用输DTO**(例外:不要将输入DTO作为输出DTO).
* 保持**数量较少**的输出DTO,尽可能**重用输DTO**(例外:不要将输入DTO作为输出DTO).
* 输出DTO可以包含比用例需要的属性**更多**的属性.
* 针对 **Create****Update** 方法,返回实体的DTO.
@ -1920,7 +1920,7 @@ public class OrganizationAppService : ApplicationService
你可能想知道为什么付款逻辑代码不在`OrganizationManager`中.付款是非常**重要的事情**,我们不能**遗漏任何一次付款**.
它确实非常重要,但是,它不能放到领域服务中.我们可能还**其它用例**来创建组织但不收取任何费用.例如:
它确实非常重要,但是,它不能放到领域服务中.我们可能还**其它用例**来创建组织但不收取任何费用.例如:
* 管理员可以在后台管理系统创建新组织,而无需支付任何费用.
* 后台作业系统导入,集成,同步组织而无需支付费用.
@ -1976,4 +1976,4 @@ public class IssueAppService
* "*Domain Driven Design*" by Eric Evans
* "*Implementing Domain Driven Design*" by Vaughn Vernon
* "*Clean Architecture*" by Robert C. Martin
* "*Clean Architecture*" by Robert C. Martin

@ -588,7 +588,7 @@ public class IdentityRoleExtendingService : ITransientDependency
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True"
}
````
@ -596,10 +596,10 @@ public class IdentityRoleExtendingService : ITransientDependency
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true",
"AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true",
"AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true",
"AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True",
"AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True"
}
````

@ -62,22 +62,25 @@ MySQL连接字符串与SQL Server连接字符串不同. 所以检查你的解决
通常需要更改 `.DbMigrator``.Web` 项目里面的 `appsettings.json` ,但它取决于你的解决方案结构.
## 更改迁移DbContext
## 更改迁移DbContext Factory
MySQL DBMS与SQL Server有一些细微的差异. 某些模块数据库映射配置(尤其是字段长度)会导致MySQL出现问题. 例如某些[IdentityServer模块](Modules/IdentityServer.md)表就存在这样的问题,它提供了一个选项可以根据你的DBMS配置字段.
启动模板包含***YourProjectName*MigrationsDbContextFactory**类这是EF Core控制台命令所必须的类比如[Add-Migration](https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/#generating--running-migrations)和[Update-Database](https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/#generating--running-migrations)在切换到MySql数据库时我们同时也需要修改`DbContextOptionsBuilder`
启动模板包含*YourProjectName*MigrationsDbContext,它负责维护和迁移数据库架构. 此DbContext基本上调用依赖模块的扩展方法来配置其数据库表.
在 *YourProjectName*MigrationsDbContextFactory 类中找到以下代码:
打开 *YourProjectName*MigrationsDbContext 更改 `builder.ConfigureIdentityServer();` 行,如下所示:
````csharp
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseSqlServer(configuration.GetConnectionString("Default"));
````
将其替换为:
````csharp
builder.ConfigureIdentityServer(options =>
{
options.DatabaseProvider = EfCoreDatabaseProvider.MySql;
});
var builder = new DbContextOptionsBuilder<YourProjectNameMigrationsDbContext>()
.UseMySql(configuration.GetConnectionString("Default"));
````
然后 `ConfigureIdentityServer()` 方法会将字段长度设置为超过MySQL的限制. 如果在创建或执行数据库迁移时遇到任何问题请参考相关模块文档.
如果在创建或执行数据库迁移时遇到任何问题请参考相关模块文档
## 重新生成迁移
@ -105,5 +108,6 @@ builder.ConfigureIdentityServer(options =>
options.DatabaseProvider = EfCoreDatabaseProvider.MySql;
});
```
v2.9+版本无需手动设置 ([版本历史](https://github.com/abpframework/abp/blob/dev/modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/EntityFrameworkCore/IdentityServerModelBuilderConfigurationOptions.cs))
相关讨论: https://github.com/abpframework/abp/issues/1920
相关讨论: https://github.com/abpframework/abp/issues/1920

@ -47,7 +47,7 @@ ABP框架的[文档](docs.abp.io)也是使用的此模块.
```json
{
"ConnectionStrings": {
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True"
}
}
```

@ -843,7 +843,7 @@ Swagger UI已配置,是此服务的默认页面. 如果你导航到URL`http://lo
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true",
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True",
"Blogging": "mongodb://localhost/MsDemo_Blogging"
}
````
@ -969,8 +969,8 @@ public class ProductServiceMigrationDbContext : AbpDbContext<ProductServiceMigra
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true",
"ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True",
"ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True"
}
````

@ -25,26 +25,29 @@ namespace Microsoft.Extensions.DependencyInjection
{
options.ClaimActions.MapAbpClaimTypes();
options.Events = new OpenIdConnectEvents
configureOptions?.Invoke(options);
options.Events ??= new OpenIdConnectEvents();
var authorizationCodeReceived = options.Events.OnAuthorizationCodeReceived ?? (_ => Task.CompletedTask);
options.Events.OnAuthorizationCodeReceived = receivedContext =>
{
OnAuthorizationCodeReceived = receivedContext =>
{
var tenantKey = receivedContext.HttpContext.RequestServices
.GetRequiredService<IOptionsSnapshot<AbpAspNetCoreMultiTenancyOptions>>().Value.TenantKey;
if (receivedContext.HttpContext.Request != null &&
receivedContext.Request.Cookies.ContainsKey(tenantKey))
{
receivedContext.TokenEndpointRequest.SetParameter(tenantKey,
receivedContext.Request.Cookies[tenantKey]);
}
return Task.CompletedTask;
}
SetAbpTenantId(receivedContext);
return authorizationCodeReceived.Invoke(receivedContext);
};
configureOptions?.Invoke(options);
});
}
private static void SetAbpTenantId(AuthorizationCodeReceivedContext receivedContext)
{
var tenantKey = receivedContext.HttpContext.RequestServices
.GetRequiredService<IOptionsSnapshot<AbpAspNetCoreMultiTenancyOptions>>().Value.TenantKey;
if (receivedContext.Request.Cookies.ContainsKey(tenantKey))
{
receivedContext.TokenEndpointRequest.SetParameter(tenantKey,
receivedContext.Request.Cookies[tenantKey]);
}
}
}
}

@ -1,3 +1,9 @@
@using Volo.Abp.Ui.Branding
@using Volo.Abp.Ui.Branding
@inject IBrandingProvider BrandingProvider
<a class="navbar-brand" href="">@BrandingProvider.AppName</a>
<a class="navbar-brand" href="">
@if (!BrandingProvider.LogoUrl.IsNullOrWhiteSpace())
{
<img src="@BrandingProvider.LogoUrl" alt="@BrandingProvider.AppName" >
}
@BrandingProvider.AppName
</a>

@ -10,7 +10,7 @@
if (MenuItem.Url != null)
{
<li class="nav-item @cssClass @disabled" id="@elementId">
<a class="nav-link" href="@url">
<a class="nav-link" href="@url" target="@MenuItem.Target">
@if (MenuItem.Icon != null)
{
if (MenuItem.Icon.StartsWith("fa"))

@ -21,7 +21,7 @@
private IReadOnlyList<LanguageInfo> _otherLanguages;
private LanguageInfo _currentLanguage;
protected async override Task OnInitializedAsync()
protected override async Task OnInitializedAsync()
{
var selectedLanguageName = await JsRuntime.InvokeAsync<string>(
"localStorage.getItem",

@ -3,6 +3,7 @@
@using Volo.Abp.MultiTenancy
@inject ICurrentUser CurrentUser
@inject ICurrentTenant CurrentTenant
@inject IJSRuntime JsRuntime
@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager
<AuthorizeView>
@ -23,7 +24,7 @@
{
@foreach (var menuItem in Menu.Items)
{
<DropdownItem Clicked="@(() => NavigateTo(menuItem.Url))">@menuItem.DisplayName</DropdownItem>
<DropdownItem Clicked="@(() => NavigateToAsync(menuItem.Url, menuItem.Target))">@menuItem.DisplayName</DropdownItem>
}
}
<DropdownDivider />
@ -37,14 +38,21 @@
</AuthorizeView>
@code{
private void NavigateTo(string uri)
private async Task NavigateToAsync(string uri, string target = null)
{
Navigation.NavigateTo(uri);
if (target == "_blank")
{
await JsRuntime.InvokeVoidAsync("open", uri, target);
}
else
{
Navigation.NavigateTo(uri);
}
}
private async Task BeginSignOut()
{
await SignOutManager.SetSignOutState();
NavigateTo("authentication/logout");
await NavigateToAsync("authentication/logout");
}
}

@ -13,7 +13,7 @@ namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic
protected ApplicationMenu Menu { get; set; }
protected async override Task OnInitializedAsync()
protected override async Task OnInitializedAsync()
{
Menu = await MenuManager.GetAsync(StandardMenus.User);

@ -21,4 +21,5 @@
<UiMessageAlert />
<UiNotificationAlert />
<UiPageProgress />
</div>

@ -11,7 +11,7 @@ namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic
protected ApplicationMenu Menu { get; set; }
protected async override Task OnInitializedAsync()
protected override async Task OnInitializedAsync()
{
Menu = await MenuManager.GetAsync(StandardMenus.Main);
}

@ -12,7 +12,7 @@ namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Themes.Basic
private List<RenderFragment> ToolbarItemRenders { get; set; } = new List<RenderFragment>();
protected async override Task OnInitializedAsync()
protected override async Task OnInitializedAsync()
{
var toolbar = await ToolbarManager.GetAsync(StandardToolbars.Main);

@ -9,7 +9,7 @@
{
if (MenuItem.Url != null)
{
<a class="dropdown-item @cssClass @disabled" href="@url" id="@elementId">
<a class="dropdown-item @cssClass @disabled" href="@url" target="@MenuItem.Target" id="@elementId">
@if (MenuItem.Icon != null)
{
if (MenuItem.Icon.StartsWith("fa"))

@ -4,35 +4,19 @@ using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars
{
public class ToolbarConfigurationContext : IToolbarConfigurationContext
{
public IServiceProvider ServiceProvider { get; }
private readonly object _serviceProviderLock = new object();
private TRef LazyGetRequiredService<TRef>(Type serviceType, ref TRef reference)
{
if (reference == null)
{
lock (_serviceProviderLock)
{
if (reference == null)
{
reference = (TRef)ServiceProvider.GetRequiredService(serviceType);
}
}
}
return reference;
}
private readonly IAbpLazyServiceProvider _lazyServiceProvider;
public IAuthorizationService AuthorizationService => LazyGetRequiredService(typeof(IAuthorizationService), ref _authorizationService);
private IAuthorizationService _authorizationService;
public IAuthorizationService AuthorizationService => _lazyServiceProvider.LazyGetRequiredService<IAuthorizationService>();
private IStringLocalizerFactory _stringLocalizerFactory;
public IStringLocalizerFactory StringLocalizerFactory => LazyGetRequiredService(typeof(IStringLocalizerFactory),ref _stringLocalizerFactory);
public IStringLocalizerFactory StringLocalizerFactory => _lazyServiceProvider.LazyGetRequiredService<IStringLocalizerFactory>();
public Toolbar Toolbar { get; }
@ -40,6 +24,7 @@ namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Toolbars
{
Toolbar = toolbar;
ServiceProvider = serviceProvider;
_lazyServiceProvider = ServiceProvider.GetRequiredService<IAbpLazyServiceProvider>();
}
public Task<bool> IsGrantedAsync(string policyName)

@ -1,6 +1,5 @@
using System;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using JetBrains.Annotations;
@ -8,7 +7,9 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp;
using Volo.Abp.AspNetCore.Components.WebAssembly;
using Volo.Abp.AspNetCore.Components.WebAssembly.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc.Client;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Modularity;
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
@ -39,13 +40,17 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
return application;
}
public async static Task InitializeAsync(
public static async Task InitializeAsync(
[NotNull] this IAbpApplicationWithExternalServiceProvider application,
[NotNull] IServiceProvider serviceProvider)
{
Check.NotNull(application, nameof(application));
Check.NotNull(serviceProvider, nameof(serviceProvider));
var serviceProviderAccessor = (WebAssemblyClientScopeServiceProviderAccessor)
serviceProvider.GetRequiredService<IClientScopeServiceProviderAccessor>();
serviceProviderAccessor.ServiceProvider = serviceProvider;
application.Initialize(serviceProvider);
using (var scope = serviceProvider.CreateScope())
@ -55,7 +60,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
}
}
private async static Task InitializeModulesAsync(IServiceProvider serviceProvider)
private static async Task InitializeModulesAsync(IServiceProvider serviceProvider)
{
foreach (var service in serviceProvider.GetServices<IAsyncInitialize>())
{
@ -63,7 +68,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
}
}
private async static Task SetCurrentLanguageAsync(IServiceScope scope)
private static async Task SetCurrentLanguageAsync(IServiceScope scope)
{
var configurationClient = scope.ServiceProvider.GetRequiredService<ICachedApplicationConfigurationClient>();
var utilsService = scope.ServiceProvider.GetRequiredService<IAbpUtilsService>();

@ -4,8 +4,9 @@ using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using Volo.Abp.AspNetCore.Components.Progression;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Components.WebAssembly
@ -18,6 +19,8 @@ namespace Volo.Abp.AspNetCore.Components.WebAssembly
private readonly NavigationManager _navigationManager;
private readonly IUiPageProgressService _uiPageProgressService;
private const string AntiForgeryCookieName = "XSRF-TOKEN";
private const string AntiForgeryHeaderName = "RequestVerificationToken";
@ -25,19 +28,33 @@ namespace Volo.Abp.AspNetCore.Components.WebAssembly
public AbpBlazorClientHttpMessageHandler(
IJSRuntime jsRuntime,
ICookieService cookieService,
NavigationManager navigationManager)
NavigationManager navigationManager,
IClientScopeServiceProviderAccessor clientScopeServiceProviderAccessor)
{
_jsRuntime = jsRuntime;
_cookieService = cookieService;
_navigationManager = navigationManager;
_uiPageProgressService = clientScopeServiceProviderAccessor.ServiceProvider.GetRequiredService<IUiPageProgressService>();
}
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
await SetLanguageAsync(request, cancellationToken);
await SetAntiForgeryTokenAsync(request);
try
{
await _uiPageProgressService.Go(null, options =>
{
options.Type = UiPageProgressType.Info;
});
await SetLanguageAsync(request, cancellationToken);
await SetAntiForgeryTokenAsync(request);
return await base.SendAsync(request, cancellationToken);
return await base.SendAsync(request, cancellationToken);
}
finally
{
await _uiPageProgressService.Go(-1);
}
}
private async Task SetLanguageAsync(HttpRequestMessage request, CancellationToken cancellationToken)

@ -0,0 +1,12 @@
using System;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Components.WebAssembly.DependencyInjection
{
public class WebAssemblyClientScopeServiceProviderAccessor :
IClientScopeServiceProviderAccessor,
ISingletonDependency
{
public IServiceProvider ServiceProvider { get; set; }
}
}

@ -0,0 +1,21 @@
using System;
using System.Threading.Tasks;
namespace Volo.Abp.AspNetCore.Components.Progression
{
public interface IUiPageProgressService
{
/// <summary>
/// An event raised after the notification is received.
/// </summary>
public event EventHandler<UiPageProgressEventArgs> ProgressChanged;
/// <summary>
/// Sets the progress percentage.
/// </summary>
/// <param name="percentage">Value of the progress from 0 to 100, or null for indeterminate progress.</param>
/// <param name="options">Additional options.</param>
/// <returns>Awaitable task.</returns>
Task Go(int? percentage, Action<UiPageProgressOptions> options = null);
}
}

@ -0,0 +1,16 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Components.Progression
{
public class NullUiPageProgressService : IUiPageProgressService, ISingletonDependency
{
public event EventHandler<UiPageProgressEventArgs> ProgressChanged;
public Task Go(int? percentage, Action<UiPageProgressOptions> options = null)
{
return Task.CompletedTask;
}
}
}

@ -0,0 +1,17 @@
using System;
namespace Volo.Abp.AspNetCore.Components.Progression
{
public class UiPageProgressEventArgs : EventArgs
{
public UiPageProgressEventArgs(int? percentage, UiPageProgressOptions options)
{
Percentage = percentage;
Options = options;
}
public int? Percentage { get; }
public UiPageProgressOptions Options { get; }
}
}

@ -0,0 +1,13 @@
namespace Volo.Abp.AspNetCore.Components.Progression
{
/// <summary>
/// Options to override page progress appearance.
/// </summary>
public class UiPageProgressOptions
{
/// <summary>
/// Type or color, of the page progress.
/// </summary>
public UiPageProgressType Type { get; set; }
}
}

@ -0,0 +1,11 @@
namespace Volo.Abp.AspNetCore.Components.Progression
{
public enum UiPageProgressType
{
Default,
Info,
Success,
Warning,
Error,
}
}

@ -25,7 +25,12 @@ namespace Volo.Abp.AspNetCore.MultiTenancy
protected override Task<string> GetTenantIdOrNameFromHttpContextOrNullAsync(ITenantResolveContext context, HttpContext httpContext)
{
var hostName = httpContext.Request.Host.Host.RemovePreFix(ProtocolPrefixes);
if (!httpContext.Request.Host.HasValue)
{
return Task.FromResult<string>(null);
}
var hostName = httpContext.Request.Host.Value.RemovePreFix(ProtocolPrefixes);
var extractResult = FormattedStringValueExtracter.Extract(hostName, _domainFormat, ignoreCase: true);
context.Handled = true;

@ -11,7 +11,7 @@ namespace Volo.Abp.AspNetCore.MultiTenancy
public override string Name => ContributorName;
protected async override Task<string> GetTenantIdOrNameFromHttpContextOrNullAsync(ITenantResolveContext context, HttpContext httpContext)
protected override async Task<string> GetTenantIdOrNameFromHttpContextOrNullAsync(ITenantResolveContext context, HttpContext httpContext)
{
if (!httpContext.Request.HasFormContentType)
{

@ -10,7 +10,7 @@ namespace Volo.Abp.AspNetCore.MultiTenancy
{
public abstract class HttpTenantResolveContributorBase : TenantResolveContributorBase
{
public async override Task ResolveAsync(ITenantResolveContext context)
public override async Task ResolveAsync(ITenantResolveContext context)
{
var httpContext = context.GetHttpContext();
if (httpContext == null)

@ -1,7 +1,14 @@
using System.Threading.Tasks;
using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.RequestLocalization;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings;
namespace Volo.Abp.AspNetCore.MultiTenancy
{
@ -21,10 +28,71 @@ namespace Volo.Abp.AspNetCore.MultiTenancy
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var tenant = await _tenantConfigurationProvider.GetAsync(saveResolveResult: true);
using (_currentTenant.Change(tenant?.Id, tenant?.Name))
if (tenant?.Id != _currentTenant.Id)
{
using (_currentTenant.Change(tenant?.Id, tenant?.Name))
{
var requestCulture = await TryGetRequestCultureAsync(context);
if (requestCulture != null)
{
CultureInfo.CurrentCulture = requestCulture.Culture;
CultureInfo.CurrentUICulture = requestCulture.UICulture;
AbpRequestCultureCookieHelper.SetCultureCookie(
context,
requestCulture
);
}
await next(context);
}
}
else
{
await next(context);
}
}
private async Task<RequestCulture> TryGetRequestCultureAsync(HttpContext httpContext)
{
var requestCultureFeature = httpContext.Features.Get<IRequestCultureFeature>();
/* If requestCultureFeature == null, that means the RequestLocalizationMiddleware was not used
* and we don't want to set the culture. */
if (requestCultureFeature == null)
{
return null;
}
/* If requestCultureFeature.Provider is not null, that means RequestLocalizationMiddleware
* already picked a language, so we don't need to set the default. */
if (requestCultureFeature.Provider != null)
{
return null;
}
var settingProvider = httpContext.RequestServices.GetRequiredService<ISettingProvider>();
var defaultLanguage = await settingProvider.GetOrNullAsync(LocalizationSettingNames.DefaultLanguage);
if (defaultLanguage.IsNullOrWhiteSpace())
{
return null;
}
string culture;
string uiCulture;
if (defaultLanguage.Contains(';'))
{
var splitted = defaultLanguage.Split(';');
culture = splitted[0];
uiCulture = splitted[1];
}
else
{
culture = defaultLanguage;
uiCulture = defaultLanguage;
}
return new RequestCulture(culture, uiCulture);
}
}
}

@ -16,7 +16,9 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.AspNetCore.Mvc.Contracts\Volo.Abp.AspNetCore.Mvc.Contracts.csproj" />
<ProjectReference Include="..\Volo.Abp.Authorization\Volo.Abp.Authorization.csproj" />
<ProjectReference Include="..\Volo.Abp.Caching\Volo.Abp.Caching.csproj" />
<ProjectReference Include="..\Volo.Abp.Features\Volo.Abp.Features.csproj" />
<ProjectReference Include="..\Volo.Abp.Http.Client\Volo.Abp.Http.Client.csproj" />
<ProjectReference Include="..\Volo.Abp.Localization\Volo.Abp.Localization.csproj" />
</ItemGroup>

@ -1,5 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Authorization;
using Volo.Abp.Caching;
using Volo.Abp.Features;
using Volo.Abp.Http.Client;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
@ -10,7 +12,9 @@ namespace Volo.Abp.AspNetCore.Mvc.Client
typeof(AbpHttpClientModule),
typeof(AbpAspNetCoreMvcContractsModule),
typeof(AbpCachingModule),
typeof(AbpLocalizationModule)
typeof(AbpLocalizationModule),
typeof(AbpAuthorizationModule),
typeof(AbpFeaturesModule)
)]
public class AbpAspNetCoreMvcClientCommonModule : AbpModule
{

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

Loading…
Cancel
Save