diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml deleted file mode 100644 index d7716a9d7c..0000000000 --- a/.github/workflows/spellcheck.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Documentation Checks - -on: - push: - branches: - - dev - paths: - # This ensures the check will only be run when something changes in the docs content - - "docs/en/**/*" - pull_request: - branches: - - dev - paths: - - "docs/en/**/*" -permissions: - contents: read # to fetch code (actions/checkout) -jobs: - spellcheck: - name: "Docs: Spellcheck (En)" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - name: Check out the code - - uses: actions/setup-node@v1 - name: Setup node - with: - node-version: "16" - - run: npm install -g cspell - name: Install cSpell - - run: cspell --config ./cSpell.json "docs/en/**/*.md" --no-progress # Update for path to the markdown files - name: Run cSpell diff --git a/.gitignore b/.gitignore index ccb98f81a0..a386e86320 100644 --- a/.gitignore +++ b/.gitignore @@ -326,4 +326,6 @@ deploy/_run_all_log.txt # No commit yarn.lock files in the subfolders of templates directory -templates/**/yarn.lock \ No newline at end of file +templates/**/yarn.lock +templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Logs/logs.txt +templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/Properties/launchSettings.json diff --git a/Directory.Build.props b/Directory.Build.props index a66d3f5509..43a884f556 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ 7.0.0 - 7.0.0 + 7.0.1 7.0.0 @@ -17,7 +17,7 @@ 4.3.0 - 4.0.3 + 4.1.0 2.4.1 diff --git a/README.md b/README.md index 332cf2adc5..c3c7679aa3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ABP Framework - + [](https://codecov.io/gh/abpframework/abp) [](https://www.nuget.org/packages/Volo.Abp.Core) [](https://www.nuget.org/packages/Volo.Abp.Core) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index de2f917fc9..e8850644b1 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -409,6 +409,44 @@ "OverallDiscountPrice": "Overall Discount Price", "OverallDiscountText": "Overall Discount Text", "SelectReport": "- Select Report -", - "NoDataAvailable": "No data available" + "NoDataAvailable": "No data available", + "StatisticsOfCachedContents": "Statistics of cached NuGet package contents for nuget.abp.io", + "Compact": "Compact", + "EditSettings": "Edit Settings", + "CurrentEstimatedSize": "Current Estimated Size", + "CurrentEntryCount": "Current Entry Count", + "TotalHits": "Total Hits", + "TotalMisses": "Total Misses", + "NoResponseFrom": "No response from", + "ContentCacheSlidingExpirationByDay": "Content Cache Sliding Expiration By Day", + "MaxDaysForCaching": "Max Days For Caching", + "Enabled": "Enabled", + "Menu:NugetPackagesContentCache": "NuGet Packages Content Cache", + "NugetPackagesContentCache": "NuGet Content Cache", + "SlidingExpritionByDayInfo": "Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed. This will not extend the entry lifetime beyond the absolute expiration.", + "MaxDaysForCachingInfo": "Gets or sets an absolute expiration time, relative to now.", + "CurrentEstimatedSizeInfo": "Indicates an estimated sum of all the NuGet packages' content size currently in the memory cache", + "CurrentEntryCountInfo": "Indicates the number of instances currently in the memory cache.", + "TotalHitsInfo": "Indicates the total number of cache misses. A cache hit occurs when a file is requested from a cache and the cache is able to fulfill that request.", + "TotalMissesInfo": "Indicates the total number of cache hits. A cache miss is when the cache does not contain the requested content.", + "Permission:VersionHistory": "Version History", + "Caches": "Caches", + "VersionHistories": "Version History", + "Version": "Version", + "PublishDate": "Publish Date", + "IsStableVersion": "Stable Version", + "IsActive": "Active", + "NewVersion": "New Version", + "VersionHistoryDeletionConfirmationMessage": "Are you sure you want to delete this version?", + "CreateAbpConsultantLogoInfo": "Maximum file size: 1MB Supported file types: jpg, jpeg, png, SVG, WebP", + "UrlCode": "Url Code", + "Icon": "Icon", + "Clear": "Clear", + "Permission:AbpConsultant": "ABP Consultant", + "Menu:AbpConsultants": "ABP Consultants", + "CreateAbpConsultant": "Create ABP Consultant", + "UrlCodeIsNotAvailable": "Url code is used by another ABP Consultant.", + "AbpConsultants": "ABP Consultants", + "AbpConsultant": "ABP Consultant" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json index f73d317fad..ca8ed7326d 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json @@ -26,6 +26,7 @@ "Volo.AbpIo.Domain:030009": "User not found!", "Volo.AbpIo.Domain:030010": "To purchase the trial license, you first need to activate your trial license!", "Volo.AbpIo.Domain:030011": "You cannot delete a trial license when it is purchased!", + "Volo.AbpIo.Domain:030012": "A user is entitled to have only 1 free trial period. You already used your trial license.", "Volo.AbpIo.Domain:070000": "The organization name can only contain latin letters, numbers, dots and hyphens!", "Volo.AbpIo.Domain:070001": "The company name can only contain latin letters, numbers, dots, space and hyphens!", "WantToLearn?": "Want to learn?", @@ -183,6 +184,9 @@ "BlackFriday": "BLACK FRIDAY", "ValidForExistingCustomers": "Also valid for the existing customers!", "CampaignBetweenDates": "From {0} to {1}", - "SaveUpTo": "SAVE UP TO${0}K" + "SaveUpTo": "SAVE UP TO${0}K", + "ImplementingDDD": "Implementing Domain Driven Design", + "ExploreTheEBook": "Explore the E-Book", + "ExploreTheBook": "Explore the Book" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/ar.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/ar.json index be07f9d832..b202302d8e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/ar.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/ar.json @@ -711,7 +711,7 @@ "WatchTakeCloserLookVideo": "شاهد فيديو \"Take a closer look at the code generation: ABP Suite\"!", "ConfirmedEmailAddressRequiredToStartTrial": "يجب أن يكون لديك عنوان بريد إلكتروني مؤكد لبدء ترخيص تجريبي.", "EmailVerificationMailNotSent": "تعذر إرسال بريد التحقق من البريد الإلكتروني.", - "GetConfirmationEmail": "انقر هنا للحصول على بريد إلكتروني للتأكيد إذا لم تكن قد حصلت عليه من قبل.", + "GetConfirmationEmail": "انقر هنا للحصول على بريد إلكتروني للتأكيد إذا لم تكن قد حصلت عليه من قبل.", "WhichLicenseTypeYouAreInterestedIn": "ما نوع الرخصة المهتم بها؟", "DontTakeOurWordForIt": "لا تأخذ كلمتنا لذلك...", "ReadAbpCommercialUsersWantYouToKnow": "اقرأ ما يريد مستخدمو ABP التجاري أن تعرفه", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index 8a0e47c529..51f2dd550a 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -735,7 +735,7 @@ "WatchTakeCloserLookVideo": "Watch the \"Take a closer look at the code generation: ABP Suite\" Video!", "ConfirmedEmailAddressRequiredToStartTrial": "You should have a confirmed email address in order to start a trial license.", "EmailVerificationMailNotSent": "Email verification mail couldn't send.", - "GetConfirmationEmail": "Click here to get a verification email if you haven't got it before.", + "GetConfirmationEmail": "Click here to get a verification email if you haven't got it before.", "WhichLicenseTypeYouAreInterestedIn": "Which license type you are interested in?", "DontTakeOurWordForIt": "Don't take our word for it...", "ReadAbpCommercialUsersWantYouToKnow": "Read what ABP Commercial users want you to know", @@ -806,6 +806,19 @@ "SupportPolicyFaqTitle": "What is your support policy?", "SupportPolicyFaqExplanation": "We do support only the active and the previous major version. We do not guarantee a patch release for the 3rd and older major versions. For example, if the active version is 7.0.0, we will release patch releases for both 6.x.x and 7.x.x. Besides, we provide support only for ABP Framework and ABP Commercial related issues. That means no support is given for the 3rd party applications, cloud services and other peripheral libraries used by ABP products. We will use commercially reasonable efforts to provide our customers with technical support during \"Volosoft Bilisim A.S\"s official business hours. On the other hand, we do not commit to a service-level agreement (SLA) response time, but we will try to respond to the technical issues as quickly as possible within our official working hours. Unless a special agreement is made with the customer, we only provide support at https://support.abp.io. We also have private email support, which is only available to Enterprise License holders.", "TotalDevelopers": "Total {0} developer(s)", - "CustomPurchaseExplanation": "Tailored to your specific needs" + "CustomPurchaseExplanation": "Tailored to your specific needs", + "WhereDidYouHearAboutUs": "Where did you hear about us?", + "Twitter": "Twitter", + "Facebook": "Facebook", + "Youtube": "YouTube", + "Google": "Google", + "Github": "GitHub", + "Friend": " From a friend", + "Other": "Other", + "WhereDidYouHearAboutUs_explain": "Specify ...", + "DeletingMemberWarningMessage": "\"{0}\" will be removed from the developer list. If you want, you can assign this empty seat to another developer later.", + "AdditionalInfo": "If the developer seats are above your requirements, you can reduce them. You can email at info@abp.io to remove some of your developer seats. Clearing unused developer seats will reduce the license renewal cost. If you want, you can re-purchase additional developer seats within your active license period. Note that, since there are {0} developers in this license package, you cannot reduce this number.", + "LinkExpiredErrorMessage": "The link you are trying to access is expired.", + "ExpirationDate": "Expiration Date" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/hu.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/hu.json index 8d1c67efae..b144988670 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/hu.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/hu.json @@ -735,7 +735,7 @@ "WatchTakeCloserLookVideo": "Tekintse meg a „Nézze meg közelebbről a kódgenerálást: ABP Suite” videót!", "ConfirmedEmailAddressRequiredToStartTrial": "A próbalicenc elindításához rendelkeznie kell egy megerősített e-mail címmel.", "EmailVerificationMailNotSent": "Nem sikerült elküldeni az ellenőrző e-mailt.", - "GetConfirmationEmail": "Kattintson ide, ha megerősítő e-mailt szeretne kapni, ha még nem kapta meg.", + "GetConfirmationEmail": "Kattintson ide, ha megerősítő e-mailt szeretne kapni, ha még nem kapta meg.", "WhichLicenseTypeYouAreInterestedIn": "Melyik licenctípus érdekli?", "DontTakeOurWordForIt": "Ne fogadd el a szavunkat...", "ReadAbpCommercialUsersWantYouToKnow": "Olvassa el, hogy az ABP Commercial felhasználói mit szeretnének tudni", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/tr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/tr.json index ae7a1cb656..9a21d6de0a 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/tr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/tr.json @@ -744,7 +744,7 @@ "WatchTakeCloserLookVideo": "\"Kod üretimine daha yakından bakın: ABP Suite\" videosunu izleyin!", "ConfirmedEmailAddressRequiredToStartTrial": "Deneme lisansı başlatmak için onaylanmış bir e -posta adresiniz olmalı.", "EmailVerificationMailNotSent": "E-posta doğrulama postası gönderilemedi.", - "GetConfirmationEmail": "Daha önce bir onay e-postası almadıysanız almak için buraya tıklayın.", + "GetConfirmationEmail": "Daha önce bir onay e-postası almadıysanız almak için buraya tıklayın.", "WhichLicenseTypeYouAreInterestedIn": "Hangi lisans türüyle ilgileniyorsunuz?", "BlackFridayDiscount": "Kara Cuma İndirimi" } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json index bce1fc11d7..be657892f7 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json @@ -735,7 +735,7 @@ "WatchTakeCloserLookVideo": "观看“详细了解ABP Suite 的代码生成”视频!", "ConfirmedEmailAddressRequiredToStartTrial": "你应该有一个确认的电子邮件地址,以便开始试用许可证。", "EmailVerificationMailNotSent": "电子邮件验证邮件不能发送。", - "GetConfirmationEmail": "点击这里获取确认邮件 如果你还没有收到。", + "GetConfirmationEmail": "点击这里获取确认邮件 如果你还没有收到。", "WhichLicenseTypeYouAreInterestedIn": "你感兴趣的许可证类型是什么?", "DontTakeOurWordForIt": "不要相信我们的话......", "ReadAbpCommercialUsersWantYouToKnow": "阅读 ABP Commercial 用户希望您了解到的内容", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index d693b60ac6..751c5b6481 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -409,9 +409,9 @@ "SeeTheScreenshot": "See the screenshot", "ApplicationModuleExplanation1": "Creates a reusable, fully layered application module solution.", "ApplicationModuleExplanation2": "You can use this option to create modules for your modular application.", - "Expert": "Expert", + "Expert": "ABP Expert", "Expert_": "Expert", - "Partner": "Partner", + "Partner": "ABP Partner", "Partner_": "Partnership", "WebSite": "Web Site", "Industry": "Industry", @@ -428,6 +428,7 @@ "Date": "Date", "Activity": "Activity", "Type": "Type", - "Contribution": "Contribution" + "Contribution": "Contribution", + "Info": "Info" } } diff --git a/cSpell.json b/cSpell.json deleted file mode 100644 index dd74bfb264..0000000000 --- a/cSpell.json +++ /dev/null @@ -1,151 +0,0 @@ -{ - "version": "0.2", - "language": "en", - "words": [ - "ABP's", - "abpframework", - "Antiforgery", - "appsettings", - "aspnet", - "aspnetcore", - "Autofac", - "automagically", - "Blazor", - "CQRS", - "crossfade", - "Dapr", - "Datagrid's", - "Datatable", - "datepicker", - "dismissable", - "dockerized", - "entrypoints", - "findability", - "hoverable", - "Iddict", - "IntelliCode", - "Keysize", - "Linq", - "Microservices", - "middlewares", - "Minifier", - "multitenancy", - "multitenant", - "Navs", - "Newtonsoft", - "Npgsql", - "oidc", - "overridable", - "Parameterless", - "Passwordless", - "PKCE", - "preconfigured", - "proxying", - "redirections", - "scrollbars", - "signin", - "Templating", - "textboxes", - "toolset", - "unsubscription", - "Xunit" - ], - "ignoreWords": [ - "Aliyun", - "Allibone", - "Blazorise", - "Boutwell", - "Cmskit", - "connectionstrings", - "Devart", - "Formik", - "Halil", - "Hanselman", - "hikalkan", - "Ibrahim", - "İbrahim", - "Kalkan", - "Kirti", - "Kommunity", - "Kulkarni", - "Luxon", - "malihu", - "Malik", - "Masis", - "Minio", - "NGXS", - "NSWAG", - "Scriban", - "Serilog", - "Shoudly", - "Shouldly", - "Sweetalert", - "Syncfusion", - "Telerik", - "Timeago", - "Toastr", - "Volo", - "Volosoft", - "Xeevis" - ], - "patterns": [ - { - "name": "Markdown links", - "pattern": "\\((.*)\\)", - "description": "" - }, - { - "name": "Markdown code blocks", - "pattern": "/^(\\s*`{3,}).*[\\s\\S]*?^\\1/gmx", - "description": "Taken from the cSpell example at https://cspell.org/configuration/patterns/#verbose-regular-expressions" - }, - { - "name": "Inline code blocks", - "pattern": "\\`([^\\`\\r\\n]+?)\\`", - "description": "https://stackoverflow.com/questions/41274241/how-to-capture-inline-markdown-code-but-not-a-markdown-code-fence-with-regex" - }, - { - "name": "Link contents", - "pattern": "\\", - "description": "" - }, - { - "name": "Snippet references", - "pattern": "-- snippet:(.*)", - "description": "" - }, - { - "name": "Snippet references 2", - "pattern": "\\<\\[sample:(.*)", - "description": "another kind of snippet reference" - }, - { - "name": "Multi-line code blocks", - "pattern": "/^\\s*```[\\s\\S]*?^\\s*```/gm" - }, - { - "name": "HTML Tags", - "pattern": "<[^>]*>", - "description": "Reference: https://stackoverflow.com/questions/11229831/regular-expression-to-remove-html-tags-from-a-string" - }, - { - "name": "Markdown Image", - "pattern": "!\\[(.*)\\]\\((.*)\\)" - } - ], - "ignoreRegExpList": [ - "Markdown links", - "Markdown code blocks", - "Inline code blocks", - "Link contents", - "Snippet references", - "Snippet references 2", - "Multi-line code blocks", - "HTML Tags", - "Markdown Image" - ], - "ignorePaths": [ - "**/*Release/Post.md", - "**/*Preview/POST.md" - ] -} diff --git a/docs/en/Application-Startup.md b/docs/en/Application-Startup.md index 1c5bf9b139..2839fcb19c 100644 --- a/docs/en/Application-Startup.md +++ b/docs/en/Application-Startup.md @@ -204,6 +204,7 @@ We've passed a lambda method to configure the `ApplicationName` option. Here's a * `ApplicationName`: A human-readable name for the application. It is a unique value for an application. * `Configuration`: Can be used to setup the [application configuration](Configuration.md) when it is not provided by the hosting system. It is not needed for ASP.NET Core and other .NET hosted applications. However, if you've used `AbpApplicationFactory` with an internal service provider, you can use this option to configure how the application configuration is built. +* `Environment`: Environment name for the application. * `PlugInSources`: A list of plugin sources. See the [Plug-In Modules documentation](PlugIn-Modules) to learn how to work with plugins. * `Services`: The `IServiceCollection` object that can be used to register service dependencies. You generally don't need that, because you configure your services in your [module class](Module-Development-Basics.md). However, it can be used while writing extension methods for the `AbpApplicationCreationOptions` class. @@ -253,6 +254,54 @@ The `IAbpApplication` interface extends the `IApplicationInfoAccessor` interface `IAbpApplication` is disposable. Always dispose of it before exiting your application. +## IAbpHostEnvironment + +Sometimes, while creating an application, we need to get the current hosting environment and take actions according to that. In such cases, we can use some services such as [IWebHostEnvironment](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment?view=aspnetcore-7.0) or [IWebAssemblyHostEnvironment](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.webassembly.hosting.iwebassemblyhostenvironment) provided by .NET, in the final application. + +However, we can not use these services in a class library, which is used by the final application. ABP Framework provides the `IAbpHostEnvironment` service, which allows you to get the current environment name whenever you want. `IAbpHostEnvironment` is used by the ABP Framework in several places to perform specific actions by the environment. For example, ABP Framework reduces the cache duration on the **Development** environment for some services. + +`IAbpHostEnvironment` obtains the current environment name by the following order: + +1. Gets and sets the environment name if it's specified in the `AbpApplicationCreationOptions`. +2. Tries to obtain the environment name from the `IWebHostEnvironment` or `IWebAssemblyHostEnvironment` services for ASP.NET Core & Blazor WASM applications if the environment name isn't specified in the `AbpApplicationCreationOptions`. +3. Sets the environment name as **Production**, if the environment name is not specified or can not be obtained from the services. + +You can configure the `AbpApplicationCreationOptions` [options class](Options.md) while creating the ABP application and set an environment name to its `Environment` property. You can find the `AddApplication` or `AddApplicationAsync` call in your solution (typically in the `Program.cs` file), and set the `Environment` option as shown below: + +```csharp +await builder.AddApplicationAsync(options => +{ + options.Environment = Environments.Staging; //or directly set as "Staging" +}); +``` + +Then, whenever you need to get the current environment name or check the environment, you can use the `IAbpHostEnvironment` interface: + +```csharp +public class MyDemoService +{ + private readonly IAbpHostEnvironment _abpHostEnvironment; + + public MyDemoService(IAbpHostEnvironment abpHostEnvironment) + { + _abpHostEnvironment = abpHostEnvironment; + } + + public void MyMethod() + { + var environmentName = _abpHostEnvironment.EnvironmentName; + + if (_abpHostEnvironment.IsDevelopment()) { /* ... */ } + + if (_abpHostEnvironment.IsStaging()) { /* ... */ } + + if (_abpHostEnvironment.IsProduction()) { /* ... */ } + + if (_abpHostEnvironment.IsEnvironment("custom-environment")) { /* ... */ } + } +} +``` + ## .NET Generic Host & ASP.NET Core Integrations `AbpApplicationFactory` can create a standalone ABP application container without any external dependency. However, in most cases, you will want to integrate it with [.NET's generic host](https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host) or ASP.NET Core. For such usages, ABP provides built-in extension methods to easily create an ABP application container that is well-integrated to these systems. diff --git a/docs/en/Blog-Posts/2023-01-04 v7_0_Release_Stable/POST.md b/docs/en/Blog-Posts/2023-01-04 v7_0_Release_Stable/POST.md new file mode 100644 index 0000000000..85350d5efe --- /dev/null +++ b/docs/en/Blog-Posts/2023-01-04 v7_0_Release_Stable/POST.md @@ -0,0 +1,75 @@ +# ABP.IO Platform 7.0 Final Has Been Released! + +[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 7.0 versions have been released today. + +## What's New With 7.0? + +Since all the new features are already explained in detail in the [7.0 RC Announcement Post](https://blog.abp.io/abp/ABP.IO-Platform-7.0-RC-Has-Been-Published), I will not repeat all the details again. See the [RC Blog Post](https://blog.abp.io/abp/ABP.IO-Platform-7.0-RC-Has-Been-Published) for all the features and enhancements. + +## Getting Started with 7.0 + +### Creating New Solutions + +You can create a new solution with the ABP Framework version 7.0 by either using the `abp new` command or generating the CLI command 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 more. + +### 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 it yet: + +```bash +dotnet tool install -g Volo.Abp.Cli +``` + +To update an existing installation: + +```bash +dotnet tool update -g Volo.Abp.Cli +``` + +#### Upgrading Existing Solutions with the 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. + +## Migration Guides + +There are breaking changes in this version that may affect your application. Please see the following migration documents, if you are upgrading from v6.x: + +* [ABP Framework 6.x to 7.0 Migration Guide](https://docs.abp.io/en/abp/7.0/Migration-Guides/Abp-7_0) +* [ABP Commercial 6.x to 7.0 Migration Guide](https://docs.abp.io/en/commercial/7.0/migration-guides/v7_0) + +## Community News + +### Highlights from .NET 7.0? + +Our team has closely followed the ASP.NET Core and Entity Framework Core 7.0 releases, read Microsoft's guides and documentation and adapt the changes to our ABP.IO Platform. We are proud to say that we've shipped the ABP 7.0 RC.1 based on .NET 7.0 just after Microsoft's .NET 7.0 release. + +In addition to the ABP's .NET 7.0 upgrade, our team has created 13 great articles to highlight the important features coming with ASP.NET Core 7.0 and Entity Framework Core 7.0. + +You can read [this post](https://volosoft.com/Blog/Highlights-for-ASP.NET-Entity-Framework-Core-NET-7.0) to see the list of all articles. + +### New ABP Community Posts + +In addition to [the 13 articles to highlight .NET 7.0 features written by our team]((https://volosoft.com/Blog/Highlights-for-ASP.NET-Entity-Framework-Core-NET-7.0)), here are some of the recent posts added to the [ABP Community](https://community.abp.io/): + +* [liangshiwei](https://github.com/realLiangshiwei) has created a new community article, that shows [How to Use the Weixin Authentication for MVC / Razor Page Applications](https://community.abp.io/posts/how-to-use-the-weixin-authentication-for-mvc-razor-page-applications-a33e0wti). +* [Jasen Fici](https://community.abp.io/posts/deploying-abp.io-to-an-azure-appservice-ma8kukdp) has created a new article: [Deploying abp.io to an Azure AppService](https://community.abp.io/posts/deploying-abp.io-to-an-azure-appservice-ma8kukdp). + +Thanks to the ABP Community for all the content they have published. You can also [post your ABP related (text or video) content](https://community.abp.io/articles/submit) to the ABP Community. + +## About the Next Version + +The next feature version will be 7.1. You can follow the [release planning here](https://github.com/abpframework/abp/milestones). + +Please [submit an issue](https://github.com/abpframework/abp/issues/new) if you have any problems with this version. diff --git a/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/Post.md b/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/Post.md new file mode 100644 index 0000000000..fd98089505 --- /dev/null +++ b/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/Post.md @@ -0,0 +1,68 @@ +# How to Use the Weixin Authentication for MVC / Razor Page Applications + +This guide demonstrates how to integrate Weixin to an ABP application that enables users to sign in using OAuth 2.0 with credentials. + +## Create a sandbox account + +If you don't have a production account, you can create a sendbox account for testing: https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index + +In this article we will use the sandbox account. + +> You should configure the callback domain name on the Weixin open platform + +## AddWeixin + +You need to install `AspNet.Security.OAuth.Weixin` package to your **.Web** project. + +In your **.Web** project, locate your **ApplicationWebModule** and modify `ConfigureAuthentication` method with the following: + +```csharp +private void ConfigureAuthentication(ServiceConfigurationContext context) +{ + var configuration = context.Services.GetConfiguration(); + context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); + context.Services.AddAuthentication() + .AddWeixin(options => + { + options.ClientId = configuration["Weixin:ClientId"]; + options.ClientSecret = configuration["Weixin:ClientSecret"]; + }); +} +``` + +Updating `appsettings.json` to add `Weixin` section: + +````json + "Weixin": { + "ClientId": "", + "ClientSecret": "" + } +```` + +## Web page authorization + +Now you can run the application to login with Weixin. + + + +It will redirect to weixin platform to scan the QR code. + +> The sandbox account lacks the necessary scope, so it may not work properly. + +## Official account authorization + +Updating `AddWeixin`: + +```csharp +context.Services.AddAuthentication() + .AddWeixin(options => + { + options.ClientId = configuration["Weixin:ClientId"]; + options.ClientSecret = configuration["Weixin:ClientSecret"]; + options.AuthorizationEndpoint = "https://open.weixin.qq.com/connect/oauth2/authorize"; + }); +``` + +Now you can use WeChat app to open the web application URL to login with weixin. + + \ No newline at end of file diff --git a/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/login-with-weixin.jpg b/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/login-with-weixin.jpg new file mode 100644 index 0000000000..cfb248ebc4 Binary files /dev/null and b/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/login-with-weixin.jpg differ diff --git a/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/offical-account.jpg b/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/offical-account.jpg new file mode 100644 index 0000000000..8157a4e952 Binary files /dev/null and b/docs/en/Community-Articles/2022-12-29-Use-Weixin-Authentication-for-MVC-Applications/offical-account.jpg differ diff --git a/docs/en/Customizing-Application-Modules-Extending-Entities.md b/docs/en/Customizing-Application-Modules-Extending-Entities.md index a3c9ae4e9a..c2cacb4e4c 100644 --- a/docs/en/Customizing-Application-Modules-Extending-Entities.md +++ b/docs/en/Customizing-Application-Modules-Extending-Entities.md @@ -101,7 +101,7 @@ public class MyLocalIdentityUserChangeEventHandler : ```` * `EntityChangedEventData` covers create, update and delete events for the given entity. If you need, you can subscribe to create, update and delete events individually (in the same class or different classes). -* This code will be executed **out of the local transaction**, because it listens the `EntityChanged` event. You can subscribe to the `EntityChangingEventData` to perform your event handler in **the same local (in-process) transaction** if the current [unit of work](Unit-Of-Work.md) is transactional. +* This code will be executed in the **current unit of work**, the whole process becomes transactional. > Reminder: This approach needs to change the `IdentityUser` entity in the same process contains the handler class. It perfectly works even for a clustered environment (when multiple instances of the same application are running on multiple servers). diff --git a/docs/en/Distributed-Event-Bus.md b/docs/en/Distributed-Event-Bus.md index cc63e98fcd..8a2bb60a18 100644 --- a/docs/en/Distributed-Event-Bus.md +++ b/docs/en/Distributed-Event-Bus.md @@ -296,6 +296,74 @@ This example; > Distributed event system use the [object to object mapping](Object-To-Object-Mapping.md) system to map `Product` objects to `ProductEto` objects. So, you need to configure the object mapping (`Product` -> `ProductEto`) too. You can check the [object to object mapping document](Object-To-Object-Mapping.md) to learn how to do it. +## Entity Synchronizer + +In a distributed (or microservice) system, it is typical to subscribe to change events for an [entity](Entities.md) type of another service, so you can get notifications when the subscribed entity changes. In that case, you can use ABP's Pre-Defined Events as explained in the previous section. + +If your purpose is to store your local copies of a remote entity, you typically subscribe to create, update and delete events of the remote entity and update your local database in your event handler. ABP provides a pre-built `EntitySynchronizer` base class to make that operation easier for you. + +Assume that there is a `Product` entity (probably an aggregate root entity) in a Catalog microservice, and you want to keep copies of the products in your Ordering microservice, with a local `OrderProduct` entity. In practice, properties of the `OrderProduct` class will be a subset of the `Product` properties, because not all the product data is needed in the Ordering microservice (however, you can make a full copy if you need). Also, the `OrderProduct` entity may have additional properties that are populated and used in the Ordering microservice. + +The first step to establish the synchronization is to define an ETO (Event Transfer Object) class in the Catalog microservice that is used to transfer the event data. Assuming the `Product` entity has a `Guid` key, your ETO can be as shown below: + +```` +[EventName("product")] +public class ProductEto : EntityEto +{ + // Your Product properties here... +} +```` + +`ProductEto` can be put in a shared project (DLL) that is referenced by the Catalog and the Ordering microservices. Alternatively, you can put a copy of the `ProductEto` class in the Ordering microservice if you don't want to introduce a common project dependency between the services. In this case, the `EventName` attribute becomes critical to map the `ProductEto` classes across two services (you should use the same event name). + +Once you define an ETO class, you should configure the ABP Framework to publish auto (create, update and delete) events for the `Product` entity, as explained in the previous section: + +````csharp +Configure(options => +{ + options.AutoEventSelectors.Add(); + options.EtoMappings.Add(); +}); +```` + +Finally, you should create a class in the Ordering microservice, that is derived from the `EntitySynchronizer` class: + +````csharp +public class ProductSynchronizer : EntitySynchronizer +{ + public ProductSynchronizer( + IObjectMapper objectMapper, + IRepository repository + ) : base(objectMapper, repository) + { + } +} +```` + +The main point of this class is it subscribes to the create, update and delete events of the source entity and updates the local entity in the database. It uses the [Object Mapper](Object-To-Object-Mapping.md) system to create or update the `OrderProduct` objects from the `ProductEto` objects. So, you should also configure the object mapper to make it properly work. Otherwise, you should manually perform the object mapping by overriding the `MapToEntityAsync(TSourceEntityEto)` and `MapToEntityAsync(TSourceEntityEto,TEntity)` methods in your `ProductSynchronizer` class. + +If your entity has a composite primary key (see the [Entities document](Entities.md)), then you should inherit from the `EntitySynchronizer` class (just don't use the `Guid` generic argument in the previous example) and implement `FindLocalEntityAsync` to find the entity in your local database using the `Repository`. + +`EntitySynchronizer` is compatible with the *Entity Versioning* system (see the [Entities document](Entities.md)). So, it works as expected even if the events are received as disordered. If the entity's version in your local database is newer than the entity in the received event, then the event is ignored. You should implement the `IHasEntityVersion` interface for the entity and ETO classes (for this example, you should implement for the `Product`, `ProductEto` and `OrderProduct` classes). + +If you want to ignore some type of change events, you can set `IgnoreEntityCreatedEvent`, `IgnoreEntityUpdatedEvent` and `IgnoreEntityDeletedEvent` in the constructor of your class. Example: + +````csharp +public class ProductSynchronizer + : EntitySynchronizer +{ + public ProductSynchronizer( + IObjectMapper objectMapper, + IRepository repository + ) : base(objectMapper, repository) + { + IgnoreEntityDeletedEvent = true; + } +} +```` + +> Notice that the `EntitySynchronizer` can only create/update the entities after you use it. If you have an existing system with existing data, you should manually copy the data for one time, because the `EntitySynchronizer` starts to work. + ## Transaction and Exception Handling Distributed event bus works in-process (since default implementation is `LocalDistributedEventBus`) unless you configure an actual provider (e.g. [Kafka](Distributed-Event-Bus-Kafka-Integration.md) or [RabbitMQ](Distributed-Event-Bus-RabbitMQ-Integration.md)). In-process event bus always executes event handlers in the same [unit of work](Unit-Of-Work.md) scope that you publishes the events in. That means, if an event handler throws an exception, then the related unit of work (the database transaction) is rolled back. In this way, your application logic and event handling logic becomes transactional (atomic) and consistent. If you want to ignore errors in an event handler, you must use a `try-catch` block in your handler and shouldn't re-throw the exception. diff --git a/docs/en/Entities.md b/docs/en/Entities.md index a1558a73ae..124e0adab1 100644 --- a/docs/en/Entities.md +++ b/docs/en/Entities.md @@ -324,6 +324,21 @@ It's designed as read-only and automatically invalidates a cached entity if the > See the [Entity Cache](Entity-Cache.md) documentation for more information. +## Versioning Entities + +ABP defines the `IHasEntityVersion` interface for automatic versioning of your entities. It only provides a single `EntityVersion` property, as shown in the following code block: + +````csharp +public interface IHasEntityVersion +{ + int EntityVersion { get; } +} +```` + +If you implement the `IHasEntityVersion` interface, ABP automatically increases the `EntityVersion` value whenever you update your entity. The initial `EntityVersion` value will be `0`, when you first create an entity and save to the database. + +> ABP can not increase the version if you directly execute SQL `UPDATE` commands in the database. It is your responsibility to increase the `EntityVersion` value in that case. Also, if you are using the aggregate pattern and change sub-collections of an aggregate root, it is your responsibility if you want to increase the version of the aggregate root object. + ## Extra Properties ABP defines the `IHasExtraProperties` interface that can be implemented by an entity to be able to dynamically set and get properties for the entity. `AggregateRoot` base class already implements the `IHasExtraProperties` interface. If you've derived from this class (or one of the related audit class defined above), you can directly use the API. diff --git a/docs/en/Getting-Started-React-Native.md b/docs/en/Getting-Started-React-Native.md index fe4cf478de..be0505b9f1 100644 --- a/docs/en/Getting-Started-React-Native.md +++ b/docs/en/Getting-Started-React-Native.md @@ -95,6 +95,20 @@ A React Native application running on an Android emulator or a physical phone ** Run the backend application as described in the [getting started document](Getting-Started.md). +> You should turn off the "Https Restriction" if you're using OpenIddict as a central identity management solution. Because the IOS Simulator doesn't support self-signed certificates and OpenIddict is set to only work with HTTPS by default. +## How to disable the Https-only settings of OpenIddict + + Go to MyProjectNameHttpApiHostModule.cs under the host project. Add put these codes under the `PreConfigureServices` function. + +```csharp +#if DEBUG + PreConfigure(options => { + options.UseAspNetCore() + .DisableTransportSecurityRequirement(); + }); +#endif +``` + ## How to Configure & Run the React Native Application diff --git a/docs/en/Getting-Started-Running-Solution.md b/docs/en/Getting-Started-Running-Solution.md index 44fbd3eb03..36cd5ffe0a 100644 --- a/docs/en/Getting-Started-Running-Solution.md +++ b/docs/en/Getting-Started-Running-Solution.md @@ -113,11 +113,11 @@ This is the HTTP API that is used by the web application. 3. Lastly, ensure that the {{if UI=="MVC"}}`.Web`{{else}}`.Blazor`{{end}} project is the startup project and run the application which will open a **welcome** page in your browser - + Click to the **login** button which will redirect you to the *authentication server* to login to the application: - + {{ else # Tiered != "Yes" }} @@ -125,7 +125,7 @@ Ensure that the {{if UI=="MVC"}}`.Web`{{else}}`.Blazor`{{end}} project is the st > Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. - + {{ end # Tiered }} @@ -171,7 +171,7 @@ Ensure that the `.Blazor` project is the startup project and run the application Once the application starts, click to the **Login** link on to header, which redirects you to the authentication server to enter a username and password: - + {{ else if UI == "NG" }} @@ -191,7 +191,7 @@ yarn start It may take a longer time for the first build. Once it finishes, it opens the Angular UI in your default browser with the [localhost:4200](http://localhost:4200/) address. - + {{ end }} diff --git a/docs/en/JSON.md b/docs/en/JSON.md index 27693bafda..d4fc550982 100644 --- a/docs/en/JSON.md +++ b/docs/en/JSON.md @@ -66,3 +66,9 @@ Add [Volo.Abp.Json.Newtonsoft](https://www.nuget.org/packages/Volo.Abp.Json.Newt #### AbpNewtonsoftJsonSerializerOptions - **JsonSerializerSettings(`Newtonsoft.Json.JsonSerializerSettings`)**: Global options for Newtonsoft library operations. See [here](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_JsonSerializerSettings.htm) for reference. + +## Configuring JSON options in ASP.NET Core + +You can change the JSON behavior in ASP.NET Core by configuring [JsonOptions](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonoptions) or +[MvcNewtonsoftJsonOptions](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcnewtonsoftjsonoptions)(if you use `Newtonsoft.Json`) + diff --git a/docs/en/KB/Windows-Path-Too-Long-Fix.md b/docs/en/KB/Windows-Path-Too-Long-Fix.md new file mode 100644 index 0000000000..e463288239 --- /dev/null +++ b/docs/en/KB/Windows-Path-Too-Long-Fix.md @@ -0,0 +1,10 @@ +# How to Fix "Filename too long" Error on Windows + +If you encounter the "filename too long" or "unzip" error on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). + +If you face long path errors related to Git, try the following command to enable long paths in Windows. +``` +git config --system core.longpaths true +``` + +See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path \ No newline at end of file diff --git a/docs/en/Migration-Guides/Abp-7_0.md b/docs/en/Migration-Guides/Abp-7_0.md index 0127c8ef9f..65a1614603 100644 --- a/docs/en/Migration-Guides/Abp-7_0.md +++ b/docs/en/Migration-Guides/Abp-7_0.md @@ -110,9 +110,104 @@ See https://github.com/abpframework/abp/pull/13845 for more info. > You can ignore this if you don't use CMS Kit Module. -## Oracle.EntityFrameworkCore and Devart.Data.Oracle.EFCore +## Data migration environment + +Please call `AddDataMigrationEnvironment` method in the migration project. + +```cs +using (var application = await AbpApplicationFactory.CreateAsync(options => +{ + //... + options.AddDataMigrationEnvironment(); +})) +{ + //... +} +``` + +```cs +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDataMigrationEnvironment(); +// Call AddDataMigrationEnvironment before AddApplicationAsync +await builder.AddApplicationAsync(); +//... +``` -These two packages do not yet support EF Core 7.0, If you use `AbpEntityFrameworkCoreOracleModule(Volo.Abp.EntityFrameworkCore.Oracle)` or `AbpEntityFrameworkCoreOracleDevartModule(Volo.Abp.EntityFrameworkCore.Oracle.Devart)` may not work as expected, We will release new packages as soon as they are updated. +See https://github.com/abpframework/abp/pull/13985 for more info. + +## Devart.Data.Oracle.EFCore + +The `Devart.Data.Oracle.EFCore` package do not yet support EF Core 7.0, If you use `AbpEntityFrameworkCoreOracleDevartModule(Volo.Abp.EntityFrameworkCore.Oracle.Devart)` may not work as expected, We will release new packages as soon as they are updated. See https://github.com/abpframework/abp/issues/14412 for more info. +# Changes on Angular Apps +## Added a new package `@abp/ng.oauth` +OAuth Functionality moved to a seperate package named `@abp/ng.oauth`, so ABP users should add the `@abp/ng.oauth` packages on app.module.ts. +Add the new npm package to your app. +``` +yarn add @abp/ng.oauth +// or npm i ---save @abp/ng.oauth +``` + +```typescript +// app.module.ts +import { AbpOAuthModule } from "@abp/ng.oauth"; +// ... +@NgModule({ + // ... + imports: [ + AbpOAuthModule.forRoot(), // <-- Add This + // ... + ], + // ... +}) +export class AppModule {} + +``` +## Lepton X Google-Font +If you are using LeptonX that has google fonts, the fonts were built-in the Lepton file. It's been moved to a seperate file. So the ABP user should add font-bundle in angular.json. ( under the 'yourProjectName' > 'architect' > 'build' > 'options' >'styles' ) + +// for LeptonX Lite +```json + { + input: 'node_modules/@volo/ngx-lepton-x.lite/assets/css/font-bundle.rtl.css', + inject: false, + bundleName: 'font-bundle.rtl', + }, + { + input: 'node_modules/@volo/ngx-lepton-x.lite/assets/css/font-bundle.css', + inject: false, + bundleName: 'font-bundle', + }, +``` + +// for LeptonX +```json + { + input: 'node_modules/@volosoft/ngx-lepton-x/assets/css/font-bundle.css', + inject: false, + bundleName: 'font-bundle', + }, + { + input: 'node_modules/@volosoft/ngx-lepton-x/assets/css/font-bundle.rtl.css', + inject: false, + bundleName: 'font-bundle.rtl', + }, +``` + +## Updated Side Menu Layout + +In side menu layout, eThemeLeptonXComponents.Navbar has been changed to eThemeLeptonXComponents.Toolbar, and +eThemeLeptonXComponents.Sidebar to eThemeLeptonXComponents.Navbar. + +And also added new replaceable component like Logo Component, Language Component etc. + +If you are using replaceable component system you can check [documentation](https://docs.abp.io/en/commercial/latest/themes/lepton-x/angular#customization). + + +## ng-zorro-antd-tree.css + +ng-zorro-antd-tree.css file should be in angular.json if the user uses AbpTree component or Abp-commercial. The ABP User should add this style definition on angular.json. ( under the 'yourProjectName' > 'architect' > 'build' > 'options' >'styles' ) + +{ "input": "node_modules/ng-zorro-antd/tree/style/index.min.css", "inject": false, "bundleName": "ng-zorro-antd-tree" }, diff --git a/docs/en/Migration-Guides/OpenIddict-Angular.md b/docs/en/Migration-Guides/OpenIddict-Angular.md index c3ca6a7fbb..fe0b3986ac 100644 --- a/docs/en/Migration-Guides/OpenIddict-Angular.md +++ b/docs/en/Migration-Guides/OpenIddict-Angular.md @@ -112,7 +112,7 @@ ## IdentityServer -This project is renamed to **AuthServer** after v6.0.0-rc1. You can also refactor and rename your project to *AuthServer* for easier updates in the future. +This project is renamed to **AuthServer** after v6.0.0. You can also refactor and rename your project to *AuthServer* for easier updates in the future. - In **MyApplication.IdentityServer.csproj** replace **project references**: diff --git a/docs/en/Migration-Guides/OpenIddict-Blazor-Server.md b/docs/en/Migration-Guides/OpenIddict-Blazor-Server.md index 65011f6124..93c9562a4d 100644 --- a/docs/en/Migration-Guides/OpenIddict-Blazor-Server.md +++ b/docs/en/Migration-Guides/OpenIddict-Blazor-Server.md @@ -103,7 +103,7 @@ ## IdentityServer -This project is renamed to **AuthServer** after v6.0.0-rc1. You can also refactor and rename your project to *AuthServer* for easier updates in the future. +This project is renamed to **AuthServer** after v6.0.0. You can also refactor and rename your project to *AuthServer* for easier updates in the future. - In **MyApplication.IdentityServer.csproj** replace **project references**: diff --git a/docs/en/Migration-Guides/OpenIddict-Blazor.md b/docs/en/Migration-Guides/OpenIddict-Blazor.md index 7db082808e..09e137a0a6 100644 --- a/docs/en/Migration-Guides/OpenIddict-Blazor.md +++ b/docs/en/Migration-Guides/OpenIddict-Blazor.md @@ -131,7 +131,7 @@ ## IdentityServer -This project is renamed to **AuthServer** after v6.0.0-rc1. You can also refactor and rename your project to *AuthServer* for easier updates in the future. +This project is renamed to **AuthServer** after v6.0.0. You can also refactor and rename your project to *AuthServer* for easier updates in the future. - In **MyApplication.IdentityServer.csproj** replace **project references**: diff --git a/docs/en/Migration-Guides/OpenIddict-Mvc.md b/docs/en/Migration-Guides/OpenIddict-Mvc.md index 7fd411155d..8dd5ec6c94 100644 --- a/docs/en/Migration-Guides/OpenIddict-Mvc.md +++ b/docs/en/Migration-Guides/OpenIddict-Mvc.md @@ -111,7 +111,7 @@ Replace role scope to **roles** and add **UsePkce** and **SignoutScheme** option ## IdentityServer -This project is renamed to **AuthServer** after v6.0.0-rc1. You can also refactor and rename your project to *AuthServer* for easier updates in the future. +This project is renamed to **AuthServer** after v6.0.0. You can also refactor and rename your project to *AuthServer* for easier updates in the future. - In **MyApplication.IdentityServer.csproj** replace **project references**: diff --git a/docs/en/Migration-Guides/OpenIddict-Step-by-Step.md b/docs/en/Migration-Guides/OpenIddict-Step-by-Step.md index 2638895843..8aff29c84d 100644 --- a/docs/en/Migration-Guides/OpenIddict-Step-by-Step.md +++ b/docs/en/Migration-Guides/OpenIddict-Step-by-Step.md @@ -1,6 +1,6 @@ # Migrating from IdentityServer to OpenIddict Step by Step Guide -This guide provides layer-by-layer guidance for migrating your existing application to [OpenIddict](https://github.com/openiddict/openiddict-core) from IdentityServer. ABP startup templates use `OpenIddict` OpenId provider from v6.0.0-rc1 by default and `IdentityServer` projects are renamed to `AuthServer` in tiered/separated solutions. Since OpenIddict is only available with ABP v6.0, you will need to update your existing application in order to apply OpenIddict changes. +This guide provides layer-by-layer guidance for migrating your existing application to [OpenIddict](https://github.com/openiddict/openiddict-core) from IdentityServer. ABP startup templates use `OpenIddict` OpenId provider from v6.0.0 by default and `IdentityServer` projects are renamed to `AuthServer` in tiered/separated solutions. Since OpenIddict is only available with ABP v6.0, you will need to update your existing application in order to apply OpenIddict changes. ## History We are not removing Identity Server packages and we will continue to release new versions of IdentityServer-related NuGet/NPM packages. That means you won't have an issue while upgrading to v6.0 when the stable version releases. We will continue to fix bugs in our packages for a while. ABP 7.0 will be based on .NET 7. If Identity Server continues to work with .NET 7, we will also continue to ship NuGet packages for our IDS integration. diff --git a/docs/en/Module-Entity-Extensions.md b/docs/en/Module-Entity-Extensions.md index 1a3c318c6b..47436a7dd9 100644 --- a/docs/en/Module-Entity-Extensions.md +++ b/docs/en/Module-Entity-Extensions.md @@ -312,12 +312,14 @@ An enum properties is shown as combobox (select) in the create/edit forms: Enum member name is shown on the table and forms by default. If you want to localize it, just create a new entry on your [localization](https://docs.abp.io/en/abp/latest/Localization) file: ````json -"UserType.SuperUser": "Super user" +"Enum:UserType.0": "Super user" ```` One of the following names can be used as the localization key: +* `Enum:UserType.0` * `Enum:UserType.SuperUser` +* `UserType.0` * `UserType.SuperUser` * `SuperUser` diff --git a/docs/en/Modules/Account.md b/docs/en/Modules/Account.md index 7beb5b128d..2ae77d532d 100644 --- a/docs/en/Modules/Account.md +++ b/docs/en/Modules/Account.md @@ -6,7 +6,7 @@ This module is based on [Microsoft's Identity library](https://docs.microsoft.co ## How to Install -This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. +This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. ### The Source Code diff --git a/docs/en/Modules/Audit-Logging.md b/docs/en/Modules/Audit-Logging.md index 30e1ef4e04..93a50c8387 100644 --- a/docs/en/Modules/Audit-Logging.md +++ b/docs/en/Modules/Audit-Logging.md @@ -6,7 +6,7 @@ The Audit Logging Module basically implements the `IAuditingStore` to save the a ## How to Install -This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. +This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. ### The Source Code diff --git a/docs/en/Modules/Background-Jobs.md b/docs/en/Modules/Background-Jobs.md index 6cce8a6c95..5fd57d812c 100644 --- a/docs/en/Modules/Background-Jobs.md +++ b/docs/en/Modules/Background-Jobs.md @@ -6,7 +6,7 @@ The Background Jobs module implements the `IBackgroundJobStore` interface and ma ## How to Install -This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. +This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. ### The Source Code diff --git a/docs/en/Modules/Cms-Kit/Comments.md b/docs/en/Modules/Cms-Kit/Comments.md index e5a5da592e..ef476600eb 100644 --- a/docs/en/Modules/Cms-Kit/Comments.md +++ b/docs/en/Modules/Cms-Kit/Comments.md @@ -1,10 +1,10 @@ # CMS Kit: Comments -CMS kit provides a **comment** system to add comments feature to any kind of resource, like blog posts, products, etc. +CMS kit provides a **comment** system to add the comment feature to any kind of resource, like blog posts, products, etc. ## Options -The comment system provides a mechanism to group comment definitions by entity types. For example, if you want to use comment system for blog posts and products, you need to define two entity types named `BlogPosts` and `Product`, and add comments under these entity types. +The comment system provides a mechanism to group comment definitions by entity types. For example, if you want to use the comment system for blog posts and products, you need to define two entity types named `BlogPosts` and `Product`, and add comments under these entity types. `CmsKitCommentOptions` can be configured in the domain layer, in the `ConfigureServices` method of your [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics). Example: @@ -12,6 +12,7 @@ The comment system provides a mechanism to group comment definitions by entity t Configure(options => { options.EntityTypes.Add(new CommentEntityTypeDefinition("Product")); + options.IsRecaptchaEnabled = true; //false by default }); ``` @@ -20,6 +21,7 @@ Configure(options => `CmsKitCommentOptions` properties: - `EntityTypes`: List of defined entity types(`CmsKitCommentOptions`) in the comment system. +- `IsRecaptchaEnabled`: This flag enables or disables the reCaptcha for the comment system. You can set it as **true** if you want to use reCaptcha in your comment system. `CommentEntityTypeDefinition` properties: @@ -33,11 +35,12 @@ The comment system provides a commenting [widget](../../UI/AspNetCore/Widgets.md @await Component.InvokeAsync(typeof(CommentingViewComponent), new { entityType = "Product", - entityId = "..." + entityId = "...", + referralLinks = new [] {"nofollow"} }) ``` -`entityType` was explained in the previous section. `entityId` should be the unique id of the product, in this example. If you have a Product entity, you can use its Id here. +`entityType` was explained in the previous section. `entityId` should be the unique id of the product, in this example. If you have a Product entity, you can use its Id here. `referralLinks` is an optional parameter. You can use this parameter to add values (such as "nofollow", "noreferrer", or any other values) to the [rel attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel) of links. ## User Interface @@ -77,7 +80,7 @@ A comment represents a written comment from a user. This module follows the [Repository Best Practices & Conventions](https://docs.abp.io/en/abp/latest/Best-Practices/Repositories) guide. -Following custom repositories are defined for this feature: +The following custom repositories are defined for this feature: - `ICommentRepository` @@ -93,8 +96,8 @@ This module follows the [Domain Services Best Practices & Conventions](https://d #### Application services -- `CommentAdminAppService` (implements `ICommentAdminAppService`): Implements the use cases of comment management system, like listing or removing comments etc. -- `CommentPublicAppService` (implements `ICommentPublicAppService`): Implements the use cases of comment management on the public websites, like listing comments, adding comments etc. +- `CommentAdminAppService` (implements `ICommentAdminAppService`): Implements the use cases of the comment management system, like listing or removing comments etc. +- `CommentPublicAppService` (implements `ICommentPublicAppService`): Implements the use cases of the comment management on the public websites, like listing comments, adding comments etc. ### Database providers diff --git a/docs/en/Modules/Docs.md b/docs/en/Modules/Docs.md index 4777275cd6..143ec7168d 100644 --- a/docs/en/Modules/Docs.md +++ b/docs/en/Modules/Docs.md @@ -28,9 +28,11 @@ If you do not have an existing ABP project, this step shows you how to create a It is recommended to use ABP CLI to create new projects. Use the following command: -`abp new Acme.MyProject` +```bash +abp new Acme.MyProject +``` -You can also navigate to https://abp.io/get-started. Enter your project name as `Acme.MyProject`, other use default options. +You can also generate a CLI command from [get started page](https://abp.io/get-started). Enter your project name as `Acme.MyProject`, other use default options. Note that this document covers `Entity Framework Core` provider but you can also select `MongoDB` as your database provider. @@ -66,7 +68,9 @@ Docs module packages are hosted on NuGet. There are 4 packages that needs be to It is recommended to use the ABP CLI to install the module, open the CMD window in the solution file (`.sln`) directory, and run the following command: -`abp add-module Volo.Docs` +```bash +abp add-module Volo.Docs +``` #### 3.2- Manually install @@ -74,19 +78,27 @@ Or you can also manually install nuget package to each project: * Install [Volo.Docs.Domain](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget package to `Acme.MyProject.Domain` project. - `Install-Package Volo.Docs.Domain` + ```bash + Install-Package Volo.Docs.Domain + ``` * Install [Volo.Docs.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Docs.EntityFrameworkCore/) nuget package to `Acme.MyProject.EntityFrameworkCore` project. - `Install-Package Volo.Docs.EntityFrameworkCore` + ```bash + Install-Package Volo.Docs.EntityFrameworkCore + ``` * Install [Volo.Docs.Application](https://www.nuget.org/packages/Volo.Docs.Application/) nuget package to `Acme.MyProject.Application` project. - `Install-Package Volo.Docs.Application` + ```bash + Install-Package Volo.Docs.Application + ``` * Install [Volo.Docs.Web](https://www.nuget.org/packages/Volo.Docs.Domain/) nuget package to `Acme.MyProject.Web` project. - `Install-Package Volo.Docs.Web` + ```bash + Install-Package Volo.Docs.Web + ``` ##### 3.2.1- Adding Module Dependencies @@ -258,7 +270,7 @@ If you choose Entity Framework as your database provider, you need to configure The default route for Docs module is; -``` +```txt /Documents ``` @@ -307,7 +319,7 @@ The new menu item for Docs Module is added to the menu. Run your web application You will see a warning says; -``` +```txt There are no projects yet! ``` @@ -487,14 +499,14 @@ For example [Getting-Started.md](https://github.com/abpio/abp-commercial-docs/bl ``` ..... -````json +```json //[doc-params] { "UI": ["MVC","NG"], "DB": ["EF", "Mongo"], "Tiered": ["Yes", "No"] } -```` +``` ........ ``` @@ -530,9 +542,9 @@ For example: You can also use variables in a text, adding **_Value** postfix to its key: -```` +```txt This document assumes that you prefer to use **{{ UI_Value }}** as the UI framework and **{{ DB_Value }}** as the database provider. -```` +``` Also, **Document_Language_Code** and **Document_Version** keys are pre-defined if you want to get the language code or the version of the current document (This may be useful for creating links that redirects to another documentation system in another domain). @@ -603,7 +615,7 @@ Finally a new Docs Module is added to your project which is feeded with GitHub. The Docs module supports full-text search using Elastic Search. It is not enabled by default. You can configure `DocsElasticSearchOptions` to enable it. -``` +```csharp Configure(options => { options.Enable = true; @@ -616,7 +628,7 @@ The `Index` is automatically created after the application starts if the `Index` `DefaultElasticClientProvider` is responsible for creating `IElasticClient`. By default, it reads Elastic Search's `Url` from `IConfiguration`. If your `IElasticClient` needs additional configuration, please use override `IElasticClientProvider` service and replace it in the [dependency injection](../Dependency-Injection.md) system. -``` +```json { "ElasticSearch": { "Url": "http://localhost:9200" diff --git a/docs/en/Modules/Feature-Management.md b/docs/en/Modules/Feature-Management.md index 9677e2f470..d6cf470391 100644 --- a/docs/en/Modules/Feature-Management.md +++ b/docs/en/Modules/Feature-Management.md @@ -6,7 +6,7 @@ The Feature Management module implements the `IFeatureManagementStore` interface ## How to Install -This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. +This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. ### The Source Code diff --git a/docs/en/Modules/Identity.md b/docs/en/Modules/Identity.md index 4cd8bef316..f73e8073b2 100644 --- a/docs/en/Modules/Identity.md +++ b/docs/en/Modules/Identity.md @@ -4,7 +4,7 @@ Identity module is used to manage roles, users and their permissions, based on t ## How to Install -This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. +This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. ### The Source Code diff --git a/docs/en/Modules/IdentityServer.md b/docs/en/Modules/IdentityServer.md index 90e9fe19eb..f754a29687 100644 --- a/docs/en/Modules/IdentityServer.md +++ b/docs/en/Modules/IdentityServer.md @@ -4,7 +4,7 @@ IdentityServer module provides a full integration with the [IdentityServer](http ## How to Install -This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. +This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. ### The Source Code diff --git a/docs/en/Modules/OpenIddict.md b/docs/en/Modules/OpenIddict.md index 3a015011e6..cfa35b7e2c 100644 --- a/docs/en/Modules/OpenIddict.md +++ b/docs/en/Modules/OpenIddict.md @@ -4,7 +4,7 @@ OpenIddict module provides an integration with the [OpenIddict](https://github.c ## How to Install -This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as a package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. +This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as a package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. ### The Source Code @@ -299,12 +299,12 @@ PreConfigure(options => The background task that automatically removes orphaned tokens/authorizations. This can be configured by `TokenCleanupOptions` to manage it. -`TokenCleanupOptions` can be configured in the `PreConfigureServices` method of your OpenIddict [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics). +`TokenCleanupOptions` can be configured in the `ConfigureServices` method of your OpenIddict [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics). Example: ```csharp -PreConfigure(options => +Configure(options => { //Set options here... }); diff --git a/docs/en/Modules/Permission-Management.md b/docs/en/Modules/Permission-Management.md index b97ddfca6c..e77a284a42 100644 --- a/docs/en/Modules/Permission-Management.md +++ b/docs/en/Modules/Permission-Management.md @@ -6,7 +6,7 @@ This module implements the `IPermissionStore` to store and manage permissions va ## How to Install -This module comes as pre-installed (as NuGet/NPM packages) when you [create a new solution](https://abp.io/get-started) with the ABP Framework. You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. +This module comes as pre-installed (as NuGet/NPM packages). You can continue to use it as package and get updates easily, or you can include its source code into your solution (see `get-source` [CLI](../CLI.md) command) to develop your custom module. ### The Source Code diff --git a/docs/en/Road-Map.md b/docs/en/Road-Map.md index 45f33dbd9c..7cf8948515 100644 --- a/docs/en/Road-Map.md +++ b/docs/en/Road-Map.md @@ -6,7 +6,7 @@ This document provides a road map, release schedule and planned features for the ### v7.1 -The next version will be 7.1 and planned to release the stable 7.1 version in February, 2024. In the version 7.1, we will mostly focus on stabilizing and enhancing existing features, improving the developer experience, as well as adding relatively minor new features. +The next version will be 7.1 and planned to release the stable 7.1 version in February, 2023. In the version 7.1, we will mostly focus on stabilizing and enhancing existing features, improving the developer experience, as well as adding relatively minor new features. See the [7.1 milestone](https://github.com/abpframework/abp/milestone/68) for all the issues we've planned to work on. diff --git a/docs/en/Startup-Templates/Application-Single-Layer.md b/docs/en/Startup-Templates/Application-Single-Layer.md index d77f2f5f40..02a743c5ee 100644 --- a/docs/en/Startup-Templates/Application-Single-Layer.md +++ b/docs/en/Startup-Templates/Application-Single-Layer.md @@ -10,7 +10,7 @@ ABP's [Application Startup Template](Application.md) provides a well-organized a ## How to Start with It? -You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can directly create & download this startup template from the [Get Started](https://abp.io/get-started) page. In this section, we will use the ABP CLI. +You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can generate a CLI command for this startup template from the [Get Started](https://abp.io/get-started) page. In this section, we will use the ABP CLI. Firstly, install the ABP CLI if you haven't installed it before: diff --git a/docs/en/Startup-Templates/Application.md b/docs/en/Startup-Templates/Application.md index 6a47363e8c..98ccdd10d2 100644 --- a/docs/en/Startup-Templates/Application.md +++ b/docs/en/Startup-Templates/Application.md @@ -11,7 +11,7 @@ This document explains **the solution structure** and projects in details. If yo ## How to Start With? -You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can directly create & download from the [Get Started](https://abp.io/get-started) page. CLI approach is used here. +You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can generate a CLI command from the [Get Started](https://abp.io/get-started) page. CLI approach is used here. First, install the ABP CLI if you haven't installed it before: diff --git a/docs/en/Startup-Templates/Module.md b/docs/en/Startup-Templates/Module.md index afb6496ed5..faf4cb41a2 100644 --- a/docs/en/Startup-Templates/Module.md +++ b/docs/en/Startup-Templates/Module.md @@ -4,7 +4,7 @@ This template can be used to create a **reusable [application module](../Modules ## How to Start With? -You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can directly create & download from the [Get Started](https://abp.io/get-started) page. CLI approach is used here. +You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can generate a CLI command from the [Get Started](https://abp.io/get-started) page. CLI approach is used here. First, install the ABP CLI if you haven't installed before: diff --git a/docs/en/Themes/LeptonXLite/Angular.md b/docs/en/Themes/LeptonXLite/Angular.md index 950cd20c8b..7ebb649aca 100644 --- a/docs/en/Themes/LeptonXLite/Angular.md +++ b/docs/en/Themes/LeptonXLite/Angular.md @@ -20,7 +20,9 @@ To add `LeptonX-lite` into your project, `yarn add bootstrap-icons` -- Then, we need to edit the styles array in `angular.json` to replace the existing style with the new one. +- Then, we need to edit the styles array in `angular.json` to replace the existing style with the new one in the following link : + +* [Styles - Angular UI](../../UI/Angular/Theme-Configurations.md) Add the following style diff --git a/docs/en/Themes/LeptonXLite/AspNetCore.md b/docs/en/Themes/LeptonXLite/AspNetCore.md index f480141120..b134135a04 100644 --- a/docs/en/Themes/LeptonXLite/AspNetCore.md +++ b/docs/en/Themes/LeptonXLite/AspNetCore.md @@ -38,7 +38,7 @@ Configure(options => // Remove the following line - BasicThemeBundles.Styles.Global, // Add the following line instead -+ LeptonXLiteThemeBundles.Styles.Global ++ LeptonXLiteThemeBundles.Styles.Global, bundle => { bundle.AddFiles("/global-styles.css"); diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md index eb00a94e0c..0b50992274 100644 --- a/docs/en/Tutorials/Part-1.md +++ b/docs/en/Tutorials/Part-1.md @@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer * [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) -> If you encounter the "filename too long" or "unzip" error on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` +> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). {{if UI == "MVC" && DB == "EF"}} @@ -66,18 +63,17 @@ The main entity of the application is the `Book`. Create a `Books` folder (names using System; using Volo.Abp.Domain.Entities.Auditing; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +public class Book : AuditedAggregateRoot { - public class Book : AuditedAggregateRoot - { - public string Name { get; set; } + public string Name { get; set; } - public BookType Type { get; set; } + public BookType Type { get; set; } - public DateTime PublishDate { get; set; } + public DateTime PublishDate { get; set; } - public float Price { get; set; } - } + public float Price { get; set; } } ```` @@ -92,20 +88,19 @@ namespace Acme.BookStore.Books The `Book` entity uses the `BookType` enum. Create a `Books` folder (namespace) in the `Acme.BookStore.Domain.Shared` project and add a `BookType` inside it: ````csharp -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +public enum BookType { - public enum BookType - { - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry - } + Undefined, + Adventure, + Biography, + Dystopia, + Fantastic, + Horror, + Science, + ScienceFiction, + Poetry } ```` @@ -153,34 +148,33 @@ Navigate to the `OnModelCreating` method in the `BookStoreDbContext` class and a using Acme.BookStore.Books; ... -namespace Acme.BookStore.EntityFrameworkCore +namespace Acme.BookStore.EntityFrameworkCore; + +public class BookStoreDbContext : + AbpDbContext, + IIdentityDbContext, + ITenantManagementDbContext { - public class BookStoreDbContext : - AbpDbContext, - IIdentityDbContext, - ITenantManagementDbContext - { - ... + ... - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); - /* Include modules to your migration db context */ + /* Include modules to your migration db context */ - builder.ConfigurePermissionManagement(); - ... + builder.ConfigurePermissionManagement(); + ... - /* Configure your own tables/entities inside here */ + /* Configure your own tables/entities inside here */ - builder.Entity(b => - { - b.ToTable(BookStoreConsts.DbTablePrefix + "Books", - BookStoreConsts.DbSchema); - b.ConfigureByConvention(); //auto configure for the base class props - b.Property(x => x.Name).IsRequired().HasMaxLength(128); - }); - } + builder.Entity(b => + { + b.ToTable(BookStoreConsts.DbTablePrefix + "Books", + BookStoreConsts.DbSchema); + b.ConfigureByConvention(); //auto configure for the base class props + b.Property(x => x.Name).IsRequired().HasMaxLength(128); + }); } } ```` @@ -202,7 +196,7 @@ This will add a new migration class to the project:  -> If you are using Visual Studio, you may want to use the `Add-Migration Created_Book_Entity -c BookStoreDbContext` and `Update-Database -Context BookStoreDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC. +> If you are using Visual Studio, you may want to use the `Add-Migration Created_Book_Entity` and `Update-Database` commands in the *Package Manager Console (PMC)*. In this case, ensure that `Acme.BookStore.EntityFrameworkCore` is the startup project in Visual Studio and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC. {{end}} @@ -220,44 +214,43 @@ using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; -namespace Acme.BookStore +namespace Acme.BookStore; + +public class BookStoreDataSeederContributor + : IDataSeedContributor, ITransientDependency { - public class BookStoreDataSeederContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; + private readonly IRepository _bookRepository; - public BookStoreDataSeederContributor(IRepository bookRepository) - { - _bookRepository = bookRepository; - } + public BookStoreDataSeederContributor(IRepository bookRepository) + { + _bookRepository = bookRepository; + } - public async Task SeedAsync(DataSeedContext context) + public async Task SeedAsync(DataSeedContext context) + { + if (await _bookRepository.GetCountAsync() <= 0) { - if (await _bookRepository.GetCountAsync() <= 0) - { - await _bookRepository.InsertAsync( - new Book - { - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - }, - autoSave: true - ); - - await _bookRepository.InsertAsync( - new Book - { - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - }, - autoSave: true - ); - } + await _bookRepository.InsertAsync( + new Book + { + Name = "1984", + Type = BookType.Dystopia, + PublishDate = new DateTime(1949, 6, 8), + Price = 19.84f + }, + autoSave: true + ); + + await _bookRepository.InsertAsync( + new Book + { + Name = "The Hitchhiker's Guide to the Galaxy", + Type = BookType.ScienceFiction, + PublishDate = new DateTime(1995, 9, 27), + Price = 42.0f + }, + autoSave: true + ); } } } @@ -290,18 +283,17 @@ In this section, you will create an application service to get, create, update a using System; using Volo.Abp.Application.Dtos; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +public class BookDto : AuditedEntityDto { - public class BookDto : AuditedEntityDto - { - public string Name { get; set; } + public string Name { get; set; } - public BookType Type { get; set; } + public BookType Type { get; set; } - public DateTime PublishDate { get; set; } + public DateTime PublishDate { get; set; } - public float Price { get; set; } - } + public float Price { get; set; } } ```` @@ -315,14 +307,13 @@ It will be needed to map the `Book` entities to the `BookDto` objects while retu using Acme.BookStore.Books; using AutoMapper; -namespace Acme.BookStore +namespace Acme.BookStore; + +public class BookStoreApplicationAutoMapperProfile : Profile { - public class BookStoreApplicationAutoMapperProfile : Profile + public BookStoreApplicationAutoMapperProfile() { - public BookStoreApplicationAutoMapperProfile() - { - CreateMap(); - } + CreateMap(); } } ```` @@ -337,24 +328,23 @@ Create a `CreateUpdateBookDto` class in the `Books` folder (namespace) of the `A using System; using System.ComponentModel.DataAnnotations; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +public class CreateUpdateBookDto { - public class CreateUpdateBookDto - { - [Required] - [StringLength(128)] - public string Name { get; set; } + [Required] + [StringLength(128)] + public string Name { get; set; } - [Required] - public BookType Type { get; set; } = BookType.Undefined; + [Required] + public BookType Type { get; set; } = BookType.Undefined; - [Required] - [DataType(DataType.Date)] - public DateTime PublishDate { get; set; } = DateTime.Now; + [Required] + [DataType(DataType.Date)] + public DateTime PublishDate { get; set; } = DateTime.Now; - [Required] - public float Price { get; set; } - } + [Required] + public float Price { get; set; } } ```` @@ -367,15 +357,14 @@ As done to the `BookDto` above, we should define the mapping from the `CreateUpd using Acme.BookStore.Books; using AutoMapper; -namespace Acme.BookStore +namespace Acme.BookStore; + +public class BookStoreApplicationAutoMapperProfile : Profile { - public class BookStoreApplicationAutoMapperProfile : Profile + public BookStoreApplicationAutoMapperProfile() { - public BookStoreApplicationAutoMapperProfile() - { - CreateMap(); - CreateMap(); - } + CreateMap(); + CreateMap(); } } ```` @@ -389,17 +378,16 @@ using System; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +public interface IBookAppService : + ICrudAppService< //Defines CRUD methods + BookDto, //Used to show books + Guid, //Primary key of the book entity + PagedAndSortedResultRequestDto, //Used for paging/sorting + CreateUpdateBookDto> //Used to create/update a book { - public interface IBookAppService : - ICrudAppService< //Defines CRUD methods - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto> //Used to create/update a book - { - } } ```` @@ -417,22 +405,21 @@ using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +public class BookAppService : + CrudAppService< + Book, //The Book entity + BookDto, //Used to show books + Guid, //Primary key of the book entity + PagedAndSortedResultRequestDto, //Used for paging/sorting + CreateUpdateBookDto>, //Used to create/update a book + IBookAppService //implement the IBookAppService { - public class BookAppService : - CrudAppService< - Book, //The Book entity - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto>, //Used to create/update a book - IBookAppService //implement the IBookAppService + public BookAppService(IRepository repository) + : base(repository) { - public BookAppService(IRepository repository) - : base(repository) - { - } } } ```` diff --git a/docs/en/Tutorials/Part-10.md b/docs/en/Tutorials/Part-10.md index 6418db7e71..6178ebbb8e 100644 --- a/docs/en/Tutorials/Part-10.md +++ b/docs/en/Tutorials/Part-10.md @@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer * [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) -> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` +> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). ## Introduction @@ -55,7 +52,7 @@ public Guid AuthorId { get; set; } {{if DB=="EF"}} -> In this tutorial, we preferred to not add a **navigation property** to the `Author` entity from the `Book` class (like `public Author Author { get; set; }`). This is due to follow the DDD best practices (rule: refer to other aggregates only by id). However, you can add such a navigation property and configure it for the EF Core. In this way, you don't need to write join queries while getting books with their authors (like we will done below) which makes your application code simpler. +> In this tutorial, we preferred to not add a **navigation property** to the `Author` entity from the `Book` class (like `public Author Author { get; set; }`). This is due to follow the DDD best practices (rule: refer to other aggregates only by id). However, you can add such a navigation property and configure it for the EF Core. In this way, you don't need to write join queries while getting books with their authors (like we will be doing below) which makes your application code simpler. {{end}} @@ -105,6 +102,7 @@ This should create a new migration class with the following code in its `Up` met migrationBuilder.AddColumn( name: "AuthorId", table: "AppBooks", + type: "uniqueidentifier", nullable: false, defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); @@ -143,72 +141,71 @@ using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; -namespace Acme.BookStore +namespace Acme.BookStore; + +public class BookStoreDataSeederContributor + : IDataSeedContributor, ITransientDependency { - public class BookStoreDataSeederContributor - : IDataSeedContributor, ITransientDependency + private readonly IRepository _bookRepository; + private readonly IAuthorRepository _authorRepository; + private readonly AuthorManager _authorManager; + + public BookStoreDataSeederContributor( + IRepository bookRepository, + IAuthorRepository authorRepository, + AuthorManager authorManager) + { + _bookRepository = bookRepository; + _authorRepository = authorRepository; + _authorManager = authorManager; + } + + public async Task SeedAsync(DataSeedContext context) { - private readonly IRepository _bookRepository; - private readonly IAuthorRepository _authorRepository; - private readonly AuthorManager _authorManager; - - public BookStoreDataSeederContributor( - IRepository bookRepository, - IAuthorRepository authorRepository, - AuthorManager authorManager) + if (await _bookRepository.GetCountAsync() > 0) { - _bookRepository = bookRepository; - _authorRepository = authorRepository; - _authorManager = authorManager; + return; } - public async Task SeedAsync(DataSeedContext context) - { - if (await _bookRepository.GetCountAsync() > 0) + var orwell = await _authorRepository.InsertAsync( + await _authorManager.CreateAsync( + "George Orwell", + new DateTime(1903, 06, 25), + "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." + ) + ); + + var douglas = await _authorRepository.InsertAsync( + await _authorManager.CreateAsync( + "Douglas Adams", + new DateTime(1952, 03, 11), + "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." + ) + ); + + await _bookRepository.InsertAsync( + new Book { - return; - } - - var orwell = await _authorRepository.InsertAsync( - await _authorManager.CreateAsync( - "George Orwell", - new DateTime(1903, 06, 25), - "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." - ) - ); - - var douglas = await _authorRepository.InsertAsync( - await _authorManager.CreateAsync( - "Douglas Adams", - new DateTime(1952, 03, 11), - "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." - ) - ); - - await _bookRepository.InsertAsync( - new Book - { - AuthorId = orwell.Id, // SET THE AUTHOR - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - }, - autoSave: true - ); - - await _bookRepository.InsertAsync( - new Book - { - AuthorId = douglas.Id, // SET THE AUTHOR - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - }, - autoSave: true - ); - } + AuthorId = orwell.Id, // SET THE AUTHOR + Name = "1984", + Type = BookType.Dystopia, + PublishDate = new DateTime(1949, 6, 8), + Price = 19.84f + }, + autoSave: true + ); + + await _bookRepository.InsertAsync( + new Book + { + AuthorId = douglas.Id, // SET THE AUTHOR + Name = "The Hitchhiker's Guide to the Galaxy", + Type = BookType.ScienceFiction, + PublishDate = new DateTime(1995, 9, 27), + Price = 42.0f + }, + autoSave: true + ); } } ```` @@ -250,22 +247,21 @@ The final `BookDto` class should be following: using System; using Volo.Abp.Application.Dtos; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +public class BookDto : AuditedEntityDto { - public class BookDto : AuditedEntityDto - { - public Guid AuthorId { get; set; } + public Guid AuthorId { get; set; } - public string AuthorName { get; set; } + public string AuthorName { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public BookType Type { get; set; } + public BookType Type { get; set; } - public DateTime PublishDate { get; set; } + public DateTime PublishDate { get; set; } - public float Price { get; set; } - } + public float Price { get; set; } } ``` @@ -285,12 +281,11 @@ Create a new class, `AuthorLookupDto`, inside the `Books` folder of the `Acme.Bo using System; using Volo.Abp.Application.Dtos; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +public class AuthorLookupDto : EntityDto { - public class AuthorLookupDto : EntityDto - { - public string Name { get; set; } - } + public string Name { get; set; } } ```` @@ -306,18 +301,17 @@ using System.Threading.Tasks; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +public interface IBookAppService : + ICrudAppService< //Defines CRUD methods + BookDto, //Used to show books + Guid, //Primary key of the book entity + PagedAndSortedResultRequestDto, //Used for paging/sorting + CreateUpdateBookDto> //Used to create/update a book { - public interface IBookAppService : - ICrudAppService< //Defines CRUD methods - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto> //Used to create/update a book - { - // ADD the NEW METHOD - Task> GetAuthorLookupAsync(); - } + // ADD the NEW METHOD + Task> GetAuthorLookupAsync(); } ```` @@ -343,119 +337,118 @@ using Volo.Abp.Application.Services; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +[Authorize(BookStorePermissions.Books.Default)] +public class BookAppService : + CrudAppService< + Book, //The Book entity + BookDto, //Used to show books + Guid, //Primary key of the book entity + PagedAndSortedResultRequestDto, //Used for paging/sorting + CreateUpdateBookDto>, //Used to create/update a book + IBookAppService //implement the IBookAppService { - [Authorize(BookStorePermissions.Books.Default)] - public class BookAppService : - CrudAppService< - Book, //The Book entity - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto>, //Used to create/update a book - IBookAppService //implement the IBookAppService + private readonly IAuthorRepository _authorRepository; + + public BookAppService( + IRepository repository, + IAuthorRepository authorRepository) + : base(repository) { - private readonly IAuthorRepository _authorRepository; + _authorRepository = authorRepository; + GetPolicyName = BookStorePermissions.Books.Default; + GetListPolicyName = BookStorePermissions.Books.Default; + CreatePolicyName = BookStorePermissions.Books.Create; + UpdatePolicyName = BookStorePermissions.Books.Edit; + DeletePolicyName = BookStorePermissions.Books.Create; + } - public BookAppService( - IRepository repository, - IAuthorRepository authorRepository) - : base(repository) + public override async Task GetAsync(Guid id) + { + //Get the IQueryable from the repository + var queryable = await Repository.GetQueryableAsync(); + + //Prepare a query to join books and authors + var query = from book in queryable + join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id + where book.Id == id + select new { book, author }; + + //Execute the query and get the book with author + var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query); + if (queryResult == null) { - _authorRepository = authorRepository; - GetPolicyName = BookStorePermissions.Books.Default; - GetListPolicyName = BookStorePermissions.Books.Default; - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Create; + throw new EntityNotFoundException(typeof(Book), id); } - public override async Task GetAsync(Guid id) - { - //Get the IQueryable from the repository - var queryable = await Repository.GetQueryableAsync(); - - //Prepare a query to join books and authors - var query = from book in queryable - join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id - where book.Id == id - select new { book, author }; - - //Execute the query and get the book with author - var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query); - if (queryResult == null) - { - throw new EntityNotFoundException(typeof(Book), id); - } + var bookDto = ObjectMapper.Map(queryResult.book); + bookDto.AuthorName = queryResult.author.Name; + return bookDto; + } - var bookDto = ObjectMapper.Map(queryResult.book); - bookDto.AuthorName = queryResult.author.Name; - return bookDto; - } + public override async Task> GetListAsync(PagedAndSortedResultRequestDto input) + { + //Get the IQueryable from the repository + var queryable = await Repository.GetQueryableAsync(); - public override async Task> GetListAsync(PagedAndSortedResultRequestDto input) - { - //Get the IQueryable from the repository - var queryable = await Repository.GetQueryableAsync(); + //Prepare a query to join books and authors + var query = from book in queryable + join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id + select new {book, author}; - //Prepare a query to join books and authors - var query = from book in queryable - join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id - select new {book, author}; + //Paging + query = query + .OrderBy(NormalizeSorting(input.Sorting)) + .Skip(input.SkipCount) + .Take(input.MaxResultCount); - //Paging - query = query - .OrderBy(NormalizeSorting(input.Sorting)) - .Skip(input.SkipCount) - .Take(input.MaxResultCount); + //Execute the query and get a list + var queryResult = await AsyncExecuter.ToListAsync(query); - //Execute the query and get a list - var queryResult = await AsyncExecuter.ToListAsync(query); + //Convert the query result to a list of BookDto objects + var bookDtos = queryResult.Select(x => + { + var bookDto = ObjectMapper.Map(x.book); + bookDto.AuthorName = x.author.Name; + return bookDto; + }).ToList(); - //Convert the query result to a list of BookDto objects - var bookDtos = queryResult.Select(x => - { - var bookDto = ObjectMapper.Map(x.book); - bookDto.AuthorName = x.author.Name; - return bookDto; - }).ToList(); + //Get the total count with another query + var totalCount = await Repository.GetCountAsync(); - //Get the total count with another query - var totalCount = await Repository.GetCountAsync(); + return new PagedResultDto( + totalCount, + bookDtos + ); + } - return new PagedResultDto( - totalCount, - bookDtos - ); - } + public async Task> GetAuthorLookupAsync() + { + var authors = await _authorRepository.GetListAsync(); - public async Task> GetAuthorLookupAsync() - { - var authors = await _authorRepository.GetListAsync(); + return new ListResultDto( + ObjectMapper.Map, List>(authors) + ); + } - return new ListResultDto( - ObjectMapper.Map, List>(authors) - ); + private static string NormalizeSorting(string sorting) + { + if (sorting.IsNullOrEmpty()) + { + return $"book.{nameof(Book.Name)}"; } - private static string NormalizeSorting(string sorting) + if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase)) { - if (sorting.IsNullOrEmpty()) - { - return $"book.{nameof(Book.Name)}"; - } - - if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase)) - { - return sorting.Replace( - "authorName", - "author.Name", - StringComparison.OrdinalIgnoreCase - ); - } - - return $"book.{sorting}"; + return sorting.Replace( + "authorName", + "author.Name", + StringComparison.OrdinalIgnoreCase + ); } + + return $"book.{sorting}"; } } ``` @@ -487,108 +480,107 @@ using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +[Authorize(BookStorePermissions.Books.Default)] +public class BookAppService : + CrudAppService< + Book, //The Book entity + BookDto, //Used to show books + Guid, //Primary key of the book entity + PagedAndSortedResultRequestDto, //Used for paging/sorting + CreateUpdateBookDto>, //Used to create/update a book + IBookAppService //implement the IBookAppService { - [Authorize(BookStorePermissions.Books.Default)] - public class BookAppService : - CrudAppService< - Book, //The Book entity - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto>, //Used to create/update a book - IBookAppService //implement the IBookAppService - { - private readonly IAuthorRepository _authorRepository; + private readonly IAuthorRepository _authorRepository; - public BookAppService( - IRepository repository, - IAuthorRepository authorRepository) - : base(repository) - { - _authorRepository = authorRepository; - GetPolicyName = BookStorePermissions.Books.Default; - GetListPolicyName = BookStorePermissions.Books.Default; - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Create; - } + public BookAppService( + IRepository repository, + IAuthorRepository authorRepository) + : base(repository) + { + _authorRepository = authorRepository; + GetPolicyName = BookStorePermissions.Books.Default; + GetListPolicyName = BookStorePermissions.Books.Default; + CreatePolicyName = BookStorePermissions.Books.Create; + UpdatePolicyName = BookStorePermissions.Books.Edit; + DeletePolicyName = BookStorePermissions.Books.Create; + } - public async override Task GetAsync(Guid id) - { - var book = await Repository.GetAsync(id); - var bookDto = ObjectMapper.Map(book); + public async override Task GetAsync(Guid id) + { + var book = await Repository.GetAsync(id); + var bookDto = ObjectMapper.Map(book); - var author = await _authorRepository.GetAsync(book.AuthorId); - bookDto.AuthorName = author.Name; + var author = await _authorRepository.GetAsync(book.AuthorId); + bookDto.AuthorName = author.Name; - return bookDto; - } + return bookDto; + } - public async override Task> - GetListAsync(PagedAndSortedResultRequestDto input) + public async override Task> + GetListAsync(PagedAndSortedResultRequestDto input) + { + //Set a default sorting, if not provided + if (input.Sorting.IsNullOrWhiteSpace()) { - //Set a default sorting, if not provided - if (input.Sorting.IsNullOrWhiteSpace()) - { - input.Sorting = nameof(Book.Name); - } + input.Sorting = nameof(Book.Name); + } - //Get the IQueryable from the repository - var queryable = await Repository.GetQueryableAsync(); + //Get the IQueryable from the repository + var queryable = await Repository.GetQueryableAsync(); - //Get the books - var books = await AsyncExecuter.ToListAsync( - queryable - .OrderBy(input.Sorting) - .Skip(input.SkipCount) - .Take(input.MaxResultCount) - ); + //Get the books + var books = await AsyncExecuter.ToListAsync( + queryable + .OrderBy(input.Sorting) + .Skip(input.SkipCount) + .Take(input.MaxResultCount) + ); - //Convert to DTOs - var bookDtos = ObjectMapper.Map, List>(books); + //Convert to DTOs + var bookDtos = ObjectMapper.Map, List>(books); - //Get a lookup dictionary for the related authors - var authorDictionary = await GetAuthorDictionaryAsync(books); + //Get a lookup dictionary for the related authors + var authorDictionary = await GetAuthorDictionaryAsync(books); - //Set AuthorName for the DTOs - bookDtos.ForEach(bookDto => bookDto.AuthorName = - authorDictionary[bookDto.AuthorId].Name); + //Set AuthorName for the DTOs + bookDtos.ForEach(bookDto => bookDto.AuthorName = + authorDictionary[bookDto.AuthorId].Name); - //Get the total count with another query (required for the paging) - var totalCount = await Repository.GetCountAsync(); + //Get the total count with another query (required for the paging) + var totalCount = await Repository.GetCountAsync(); - return new PagedResultDto( - totalCount, - bookDtos - ); - } + return new PagedResultDto( + totalCount, + bookDtos + ); + } - public async Task> GetAuthorLookupAsync() - { - var authors = await _authorRepository.GetListAsync(); + public async Task> GetAuthorLookupAsync() + { + var authors = await _authorRepository.GetListAsync(); - return new ListResultDto( - ObjectMapper.Map, List>(authors) - ); - } + return new ListResultDto( + ObjectMapper.Map, List>(authors) + ); + } - private async Task> - GetAuthorDictionaryAsync(List books) - { - var authorIds = books - .Select(b => b.AuthorId) - .Distinct() - .ToArray(); + private async Task> + GetAuthorDictionaryAsync(List books) + { + var authorIds = books + .Select(b => b.AuthorId) + .Distinct() + .ToArray(); - var queryable = await _authorRepository.GetQueryableAsync(); + var queryable = await _authorRepository.GetQueryableAsync(); - var authors = await AsyncExecuter.ToListAsync( - queryable.Where(a => authorIds.Contains(a.Id)) - ); + var authors = await AsyncExecuter.ToListAsync( + queryable.Where(a => authorIds.Contains(a.Id)) + ); - return authors.ToDictionary(x => x.Id, x => x); - } + return authors.ToDictionary(x => x.Id, x => x); } } ``` @@ -625,76 +617,76 @@ using Volo.Abp.Application.Dtos; using Volo.Abp.Validation; using Xunit; -namespace Acme.BookStore.Books -{ {{if DB=="Mongo"}} - [Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} - public class BookAppService_Tests : BookStoreApplicationTestBase +namespace Acme.BookStore.Books; + + {{if DB=="Mongo"}} +[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} +public class BookAppService_Tests : BookStoreApplicationTestBase +{ + private readonly IBookAppService _bookAppService; + private readonly IAuthorAppService _authorAppService; + + public BookAppService_Tests() { - private readonly IBookAppService _bookAppService; - private readonly IAuthorAppService _authorAppService; + _bookAppService = GetRequiredService(); + _authorAppService = GetRequiredService(); + } - public BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - _authorAppService = GetRequiredService(); - } + [Fact] + public async Task Should_Get_List_Of_Books() + { + //Act + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + + //Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "1984" && + b.AuthorName == "George Orwell"); + } - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); + [Fact] + public async Task Should_Create_A_Valid_Book() + { + var authors = await _authorAppService.GetListAsync(new GetAuthorListDto()); + var firstAuthor = authors.Items.First(); - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "1984" && - b.AuthorName == "George Orwell"); - } + //Act + var result = await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + AuthorId = firstAuthor.Id, + Name = "New test book 42", + Price = 10, + PublishDate = System.DateTime.Now, + Type = BookType.ScienceFiction + } + ); - [Fact] - public async Task Should_Create_A_Valid_Book() - { - var authors = await _authorAppService.GetListAsync(new GetAuthorListDto()); - var firstAuthor = authors.Items.First(); + //Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New test book 42"); + } - //Act - var result = await _bookAppService.CreateAsync( + [Fact] + public async Task Should_Not_Create_A_Book_Without_Name() + { + var exception = await Assert.ThrowsAsync(async () => + { + await _bookAppService.CreateAsync( new CreateUpdateBookDto { - AuthorId = firstAuthor.Id, - Name = "New test book 42", + Name = "", Price = 10, - PublishDate = System.DateTime.Now, + PublishDate = DateTime.Now, Type = BookType.ScienceFiction } ); + }); - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); - } - - [Fact] - public async Task Should_Not_Create_A_Book_Without_Name() - { - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(m => m == "Name")); - } + exception.ValidationErrors + .ShouldContain(err => err.MemberNames.Any(m => m == "Name")); } } ``` @@ -708,7 +700,7 @@ namespace Acme.BookStore.Books ### The Book List -Book list page change is trivial. Open the `Pages/Books/Index.js` in the `Acme.BookStore.Web` project and add the following column definition between the `name` and `type` columns: +Book list page change is trivial. Open the `Pages/Books/Index.js` in the `Acme.BookStore.Web` project and add an `authorName` column between the `name` and `type` columns: ````js ... @@ -727,7 +719,7 @@ Book list page change is trivial. Open the `Pages/Books/Index.js` in the `Acme.B title: l('Type'), data: "type", render: function (data) { - return l('Enum:BookType:' + data); + return l('Enum:BookType.' + data); } }, ... @@ -735,7 +727,7 @@ Book list page change is trivial. Open the `Pages/Books/Index.js` in the `Acme.B When you run the application, you can see the *Author* column on the table: - + ### Create Modal @@ -753,61 +745,60 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; -namespace Acme.BookStore.Web.Pages.Books +namespace Acme.BookStore.Web.Pages.Books; + +public class CreateModalModel : BookStorePageModel { - public class CreateModalModel : BookStorePageModel - { - [BindProperty] - public CreateBookViewModel Book { get; set; } + [BindProperty] + public CreateBookViewModel Book { get; set; } - public List Authors { get; set; } + public List Authors { get; set; } - private readonly IBookAppService _bookAppService; + private readonly IBookAppService _bookAppService; - public CreateModalModel( - IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } + public CreateModalModel( + IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } - public async Task OnGetAsync() - { - Book = new CreateBookViewModel(); + public async Task OnGetAsync() + { + Book = new CreateBookViewModel(); - var authorLookup = await _bookAppService.GetAuthorLookupAsync(); - Authors = authorLookup.Items - .Select(x => new SelectListItem(x.Name, x.Id.ToString())) - .ToList(); - } + var authorLookup = await _bookAppService.GetAuthorLookupAsync(); + Authors = authorLookup.Items + .Select(x => new SelectListItem(x.Name, x.Id.ToString())) + .ToList(); + } - public async Task OnPostAsync() - { - await _bookAppService.CreateAsync( - ObjectMapper.Map(Book) - ); - return NoContent(); - } + public async Task OnPostAsync() + { + await _bookAppService.CreateAsync( + ObjectMapper.Map(Book) + ); + return NoContent(); + } - public class CreateBookViewModel - { - [SelectItems(nameof(Authors))] - [DisplayName("Author")] - public Guid AuthorId { get; set; } + public class CreateBookViewModel + { + [SelectItems(nameof(Authors))] + [DisplayName("Author")] + public Guid AuthorId { get; set; } - [Required] - [StringLength(128)] - public string Name { get; set; } + [Required] + [StringLength(128)] + public string Name { get; set; } - [Required] - public BookType Type { get; set; } = BookType.Undefined; + [Required] + public BookType Type { get; set; } = BookType.Undefined; - [Required] - [DataType(DataType.Date)] - public DateTime PublishDate { get; set; } = DateTime.Now; + [Required] + [DataType(DataType.Date)] + public DateTime PublishDate { get; set; } = DateTime.Now; - [Required] - public float Price { get; set; } - } + [Required] + public float Price { get; set; } } } ``` @@ -832,66 +823,65 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; -namespace Acme.BookStore.Web.Pages.Books +namespace Acme.BookStore.Web.Pages.Books; + +public class EditModalModel : BookStorePageModel { - public class EditModalModel : BookStorePageModel - { - [BindProperty] - public EditBookViewModel Book { get; set; } + [BindProperty] + public EditBookViewModel Book { get; set; } - public List Authors { get; set; } + public List Authors { get; set; } - private readonly IBookAppService _bookAppService; + private readonly IBookAppService _bookAppService; - public EditModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } + public EditModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } - public async Task OnGetAsync(Guid id) - { - var bookDto = await _bookAppService.GetAsync(id); - Book = ObjectMapper.Map(bookDto); + public async Task OnGetAsync(Guid id) + { + var bookDto = await _bookAppService.GetAsync(id); + Book = ObjectMapper.Map(bookDto); - var authorLookup = await _bookAppService.GetAuthorLookupAsync(); - Authors = authorLookup.Items - .Select(x => new SelectListItem(x.Name, x.Id.ToString())) - .ToList(); - } + var authorLookup = await _bookAppService.GetAuthorLookupAsync(); + Authors = authorLookup.Items + .Select(x => new SelectListItem(x.Name, x.Id.ToString())) + .ToList(); + } - public async Task OnPostAsync() - { - await _bookAppService.UpdateAsync( - Book.Id, - ObjectMapper.Map(Book) - ); + public async Task OnPostAsync() + { + await _bookAppService.UpdateAsync( + Book.Id, + ObjectMapper.Map(Book) + ); - return NoContent(); - } + return NoContent(); + } - public class EditBookViewModel - { - [HiddenInput] - public Guid Id { get; set; } + public class EditBookViewModel + { + [HiddenInput] + public Guid Id { get; set; } - [SelectItems(nameof(Authors))] - [DisplayName("Author")] - public Guid AuthorId { get; set; } + [SelectItems(nameof(Authors))] + [DisplayName("Author")] + public Guid AuthorId { get; set; } - [Required] - [StringLength(128)] - public string Name { get; set; } + [Required] + [StringLength(128)] + public string Name { get; set; } - [Required] - public BookType Type { get; set; } = BookType.Undefined; + [Required] + public BookType Type { get; set; } = BookType.Undefined; - [Required] - [DataType(DataType.Date)] - public DateTime PublishDate { get; set; } = DateTime.Now; + [Required] + [DataType(DataType.Date)] + public DateTime PublishDate { get; set; } = DateTime.Now; - [Required] - public float Price { get; set; } - } + [Required] + public float Price { get; set; } } } ``` @@ -937,7 +927,7 @@ CreateMap(); You can run the application and try to create a new book or update an existing book. You will see a drop down list on the create/update form to select the author of the book: - + {{else if UI=="NG"}} @@ -966,13 +956,13 @@ Book list page change is trivial. Open the `/src/app/book/book.component.html` a When you run the application, you can see the *Author* column on the table: - + ### Create/Edit Forms The next step is to add an Author selection (dropdown) to the create/edit forms. The final UI will look like the one shown below: - + Added the Author dropdown as the first element in the form. @@ -1114,7 +1104,7 @@ It is very easy to show the *Author Name* in the book list. Open the `/Pages/Boo When you run the application, you can see the *Author* column on the table: - + ### Create Book Modal @@ -1136,6 +1126,21 @@ protected override async Task OnInitializedAsync() * It is essential to call the `base.OnInitializedAsync()` since `AbpCrudPageBase` has some initialization code to be executed. +Override the `OpenCreateModalAsync` method and adding the following code: + +````csharp +protected override async Task OpenCreateModalAsync() +{ + if (!authorList.Any()) + { + throw new UserFriendlyException(message: L["AnAuthorIsRequiredForCreatingBook"]); + } + + await base.OpenCreateModalAsync(); + NewEntity.AuthorId = authorList.First().Id; +} +```` + The final `@code` block should be the following: ````csharp @@ -1157,6 +1162,17 @@ The final `@code` block should be the following: await base.OnInitializedAsync(); authorList = (await AppService.GetAuthorLookupAsync()).Items; } + + protected override async Task OpenCreateModalAsync() + { + if (!authorList.Any()) + { + throw new UserFriendlyException(message: L["AnAuthorIsRequiredForCreatingBook"]); + } + + await base.OpenCreateModalAsync(); + NewEntity.AuthorId = authorList.First().Id; + } } ```` @@ -1166,7 +1182,6 @@ Finally, add the following `Field` definition into the `ModalBody` of the *Creat @L["Author"] - @L["PickAnAuthor"] @foreach (var author in authorList) { @@ -1180,12 +1195,12 @@ Finally, add the following `Field` definition into the `ModalBody` of the *Creat This requires to add a new localization key to the `en.json` file: ````js -"PickAnAuthor": "Pick an author" +"AnAuthorIsRequiredForCreatingBook": "An author is required to create a book" ```` You can run the application to see the *Author Selection* while creating a new book: - + ### Edit Book Modal diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index 3e7a1bc735..d22e4b5062 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/docs/en/Tutorials/Part-2.md @@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer * [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) -> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` +> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). {{if UI == "MVC" && DB == "EF"}} @@ -135,22 +132,22 @@ Open the `en.json` (*the English translations*) file and change the content as s "CreationTime": "Creation time", "AreYouSure": "Are you sure?", "AreYouSureToDelete": "Are you sure you want to delete this item?", - "Enum:BookType.Undefined": "Undefined", - "Enum:BookType.Adventure": "Adventure", - "Enum:BookType.Biography": "Biography", - "Enum:BookType.Dystopia": "Dystopia", - "Enum:BookType.Fantastic": "Fantastic", - "Enum:BookType.Horror": "Horror", - "Enum:BookType.Science": "Science", - "Enum:BookType.ScienceFiction": "Science fiction", - "Enum:BookType.Poetry": "Poetry" + "Enum:BookType.0": "Undefined", + "Enum:BookType.1": "Adventure", + "Enum:BookType.2": "Biography", + "Enum:BookType.3": "Dystopia", + "Enum:BookType.4": "Fantastic", + "Enum:BookType.5": "Horror", + "Enum:BookType.6": "Science", + "Enum:BookType.7": "Science fiction", + "Enum:BookType.8": "Poetry" } } ```` * Localization key names are arbitrary. You can set any name. We prefer some conventions for specific text types; * Add `Menu:` prefix for menu items. - * Use `Enum:.` or `.` or `` naming convention to localize the enum members. When you do it like that, ABP can automatically localize the enums in some proper cases. + * Use `Enum:.` or `.` naming convention to localize the enum members. When you do it like that, ABP can automatically localize the enums in some proper cases. If a text is not defined in the localization file, it **falls back** to the localization key (as ASP.NET Core's standard behavior). @@ -181,14 +178,13 @@ Open the `Index.cshtml` and change the whole content as shown below: ```csharp using Microsoft.AspNetCore.Mvc.RazorPages; -namespace Acme.BookStore.Web.Pages.Books +namespace Acme.BookStore.Web.Pages.Books; + +public class IndexModel : PageModel { - public class IndexModel : PageModel + public void OnGet() { - public void OnGet() - { - } } } ``` @@ -215,7 +211,7 @@ context.Menu.AddItem( Run the project, login to the application with the username `admin` and the password `1q2w3E*` and you can see that the new menu item has been added to the main menu: - + When you click on the Books menu item under the Book Store parent, you will be redirected to the new empty Books Page. @@ -282,7 +278,7 @@ $(function () { title: l('Type'), data: "type", render: function (data) { - return l('Enum:BookType:' + data); + return l('Enum:BookType.' + data); } }, { @@ -328,7 +324,7 @@ $(function () { You can run the application! The final UI of this part is shown below: - + This is a fully working, server side paged, sorted and localized table of books. @@ -520,7 +516,7 @@ Open the `/src/app/book/book.component.html` and replace the content as shown be - {%{{{ '::Enum:BookType:' + row.type | abpLocalization }}}%} + {%{{{ '::Enum:BookType.' + row.type | abpLocalization }}}%} @@ -540,7 +536,7 @@ Open the `/src/app/book/book.component.html` and replace the content as shown be Now you can see the final result on your browser: - + {{else if UI == "Blazor" || UI == "BlazorServer"}} @@ -584,7 +580,7 @@ context.Menu.AddItem( Run the project, login to the application with the username `admin` and the password `1q2w3E*` and see that the new menu item has been added to the main menu: - + When you click on the Books menu item under the Book Store parent, you will be redirected to the new empty Books Page. @@ -624,7 +620,7 @@ Open the `Books.razor` and replace the content as the following: Field="@nameof(BookDto.Type)" Caption="@L["Type"]"> - @L[$"Enum:BookType.{Enum.GetName(context.Type)}"] + @L[$"Enum:BookType.{context.Type}"] If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` +> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). {{if UI == "MVC" && DB == "EF"}} @@ -53,13 +50,13 @@ This part is also recorded as a video tutorial and **(bookDto); - } + public async Task OnGetAsync() + { + var bookDto = await _bookAppService.GetAsync(Id); + Book = ObjectMapper.Map(bookDto); + } - public async Task OnPostAsync() - { - await _bookAppService.UpdateAsync(Id, Book); - return NoContent(); - } + public async Task OnPostAsync() + { + await _bookAppService.UpdateAsync(Id, Book); + return NoContent(); } } ```` @@ -330,14 +326,13 @@ To be able to map the `BookDto` to `CreateUpdateBookDto`, configure a new mappin ````csharp using AutoMapper; -namespace Acme.BookStore.Web +namespace Acme.BookStore.Web; + +public class BookStoreWebAutoMapperProfile : Profile { - public class BookStoreWebAutoMapperProfile : Profile + public BookStoreWebAutoMapperProfile() { - public BookStoreWebAutoMapperProfile() - { - CreateMap(); - } + CreateMap(); } } ```` @@ -421,7 +416,7 @@ $(function () { title: l('Type'), data: "type", render: function (data) { - return l('Enum:BookType:' + data); + return l('Enum:BookType.' + data); } }, { @@ -465,7 +460,9 @@ You can run the application and edit any book by selecting the edit action on a The final UI looks as below: - + + +> Notice that you don't see the "Actions" button in the figure below. Instead, you see an "Edit" button. ABP is smart enough to show a single simple button instead of a actions dropdown button when the dropdown has only a single item. After the next section, it will turn to a drop down button. ## Deleting a Book @@ -557,7 +554,7 @@ $(function () { title: l('Type'), data: "type", render: function (data) { - return l('Enum:BookType:' + data); + return l('Enum:BookType.' + data); } }, { @@ -656,7 +653,7 @@ Open `/src/app/book/book.component.html` and make the following changes: - + {%{{{ "::NewBook" | abpLocalization }}}%} @@ -690,7 +687,7 @@ Open `/src/app/book/book.component.html` and make the following changes: You can open your browser and click the **New book** button to see the new modal. - + ### Create a Reactive Form @@ -776,25 +773,25 @@ Open `/src/app/book/book.component.html` and replace ` - + Name * - + Price * - + Type * Select a book type - {%{{{ type.key }}}%} + {%{{{ '::Enum:BookType.' + type.value | abpLocalization }}}%} - + Publish date * - {%{{{ '::Actions' | abpLocalization }}}%} + {%{{{ '::Actions' | abpLocalization }}}%} @@ -1055,7 +1052,7 @@ Open `/src/app/book/book.component.html` and add the following `ngx-datatable- Added an "Actions" dropdown as the first column of the table that is shown below: - + Also, change the `ng-template #abpHeader` section as shown below: @@ -1119,11 +1116,11 @@ Open `/src/app/book/book.component.html` and modify the `ngbDropdownMenu` to add The final actions dropdown UI looks like below: - + Clicking the "Delete" action calls the `delete` method which then shows a confirmation popup as shown below: - + {{end}} @@ -1153,7 +1150,7 @@ Open the `Books.razor` and replace the `` section with the following This will change the card header by adding a "New book" button to the right side: - + Now, we can add a modal that will be opened when we click the button. @@ -1188,7 +1185,7 @@ Open the `Books.razor` and add the following code to the end of the page: @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType))) { - @L[$"Enum:BookType.{Enum.GetName((BookType)bookTypeValue)}"] + @L[$"Enum:BookType.{bookTypeValue}"] } @@ -1227,7 +1224,7 @@ This code requires a service; Inject the `AbpBlazorMessageLocalizerHelper` at That's all. Run the application and try to add a new book: - + ## Updating a Book @@ -1253,7 +1250,7 @@ Open the `Books.razor` and add the following `DataGridEntityActionsColumn` secti The `DataGridEntityActionsColumn` component is used to show an "Actions" dropdown for each row in the `DataGrid`. The `DataGridEntityActionsColumn` shows a **single button** instead of a dropdown if there is only one available action inside it: - + ### Edit Modal @@ -1286,7 +1283,7 @@ We can now define a modal to edit the book. Add the following code to the end of @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType))) { - @L[$"Enum:BookType.{Enum.GetName((BookType)bookTypeValue)}"] + @L[$"Enum:BookType.{bookTypeValue}"] } @@ -1324,14 +1321,13 @@ Open the `BookStoreBlazorAutoMapperProfile` inside the `Acme.BookStore.Blazor` p using Acme.BookStore.Books; using AutoMapper; -namespace Acme.BookStore.Blazor +namespace Acme.BookStore.Blazor; + +public class BookStoreBlazorAutoMapperProfile : Profile { - public class BookStoreBlazorAutoMapperProfile : Profile + public BookStoreBlazorAutoMapperProfile() { - public BookStoreBlazorAutoMapperProfile() - { - CreateMap(); - } + CreateMap(); } } ```` @@ -1342,7 +1338,7 @@ namespace Acme.BookStore.Blazor You can now run the application and try to edit a book. - + > Tip: Try to leave the *Name* field empty and submit the form to show the validation error message. @@ -1363,7 +1359,7 @@ Open the `Books.razor` page and add the following `EntityAction` code under the The "Actions" button becomes a dropdown since it has two actions now: - + Run the application and try to delete a book. @@ -1423,7 +1419,7 @@ Here's the complete code to create the book management CRUD page, that has been Field="@nameof(BookDto.Type)" Caption="@L["Type"]"> - @L[$"Enum:BookType.{Enum.GetName(context.Type)}"] + @L[$"Enum:BookType.{context.Type}"] - @L[$"Enum:BookType.{Enum.GetName((BookType)bookTypeValue)}"] + @L[$"Enum:BookType.{bookTypeValue}"] } @@ -1528,7 +1524,7 @@ Here's the complete code to create the book management CRUD page, that has been @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType))) { - @L[$"Enum:BookType.{Enum.GetName((BookType)bookTypeValue)}"] + @L[$"Enum:BookType.{bookTypeValue}"] } diff --git a/docs/en/Tutorials/Part-4.md b/docs/en/Tutorials/Part-4.md index 036d8a18bd..072392e82b 100644 --- a/docs/en/Tutorials/Part-4.md +++ b/docs/en/Tutorials/Part-4.md @@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer * [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) -> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` +> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). {{if UI == "MVC" && DB == "EF"}} @@ -88,30 +85,31 @@ using Volo.Abp.Application.Dtos; using Volo.Abp.Validation; using Xunit; -namespace Acme.BookStore.Books -{ {{if DB=="Mongo"}} - [Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} - public class BookAppService_Tests : BookStoreApplicationTestBase - { - private readonly IBookAppService _bookAppService; +namespace Acme.BookStore.Books; - public BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - } +{{if DB=="Mongo"}} +[Collection(BookStoreTestConsts.CollectionDefinitionName)] +{{end}} +public class BookAppService_Tests : BookStoreApplicationTestBase +{ + private readonly IBookAppService _bookAppService; - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "1984"); - } + [Fact] + public async Task Should_Get_List_Of_Books() + { + //Act + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + + //Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "1984"); } } ```` @@ -179,69 +177,70 @@ using Volo.Abp.Application.Dtos; using Volo.Abp.Validation; using Xunit; -namespace Acme.BookStore.Books -{ {{if DB=="Mongo"}} - [Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} - public class BookAppService_Tests : BookStoreApplicationTestBase +namespace Acme.BookStore.Books; + +{{if DB=="Mongo"}} +[Collection(BookStoreTestConsts.CollectionDefinitionName)] +{{end}} +public class BookAppService_Tests : BookStoreApplicationTestBase +{ + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() { - private readonly IBookAppService _bookAppService; + _bookAppService = GetRequiredService(); + } - public BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - } + [Fact] + public async Task Should_Get_List_Of_Books() + { + //Act + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); + //Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "1984"); + } + + [Fact] + public async Task Should_Create_A_Valid_Book() + { + //Act + var result = await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "New test book 42", + Price = 10, + PublishDate = DateTime.Now, + Type = BookType.ScienceFiction + } + ); - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "1984"); - } - - [Fact] - public async Task Should_Create_A_Valid_Book() + //Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New test book 42"); + } + + [Fact] + public async Task Should_Not_Create_A_Book_Without_Name() + { + var exception = await Assert.ThrowsAsync(async () => { - //Act - var result = await _bookAppService.CreateAsync( + await _bookAppService.CreateAsync( new CreateUpdateBookDto { - Name = "New test book 42", + Name = "", Price = 10, PublishDate = DateTime.Now, Type = BookType.ScienceFiction } ); + }); - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); - } - - [Fact] - public async Task Should_Not_Create_A_Book_Without_Name() - { - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); - } + exception.ValidationErrors + .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); } } ```` diff --git a/docs/en/Tutorials/Part-5.md b/docs/en/Tutorials/Part-5.md index 810c79e736..f33a41f392 100644 --- a/docs/en/Tutorials/Part-5.md +++ b/docs/en/Tutorials/Part-5.md @@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer * [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) -> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` +> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). {{if UI == "MVC" && DB == "EF"}} @@ -58,19 +55,18 @@ A permission must have a unique name (a `string`). The best way is to define it Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project (in the `Permissions` folder) and change the content as shown below: ````csharp -namespace Acme.BookStore.Permissions +namespace Acme.BookStore.Permissions; + +public static class BookStorePermissions { - public static class BookStorePermissions + public const string GroupName = "BookStore"; + + public static class Books { - public const string GroupName = "BookStore"; - - public static class Books - { - public const string Default = GroupName + ".Books"; - public const string Create = Default + ".Create"; - public const string Edit = Default + ".Edit"; - public const string Delete = Default + ".Delete"; - } + public const string Default = GroupName + ".Books"; + public const string Create = Default + ".Create"; + public const string Edit = Default + ".Edit"; + public const string Delete = Default + ".Delete"; } } ```` @@ -88,24 +84,23 @@ using Acme.BookStore.Localization; using Volo.Abp.Authorization.Permissions; using Volo.Abp.Localization; -namespace Acme.BookStore.Permissions +namespace Acme.BookStore.Permissions; + +public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider { - public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider + public override void Define(IPermissionDefinitionContext context) { - public override void Define(IPermissionDefinitionContext context) - { - var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore")); - - var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books")); - booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create")); - booksPermission.AddChild(BookStorePermissions.Books.Edit, L("Permission:Books.Edit")); - booksPermission.AddChild(BookStorePermissions.Books.Delete, L("Permission:Books.Delete")); - } - - private static LocalizableString L(string name) - { - return LocalizableString.Create(name); - } + var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore")); + + var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books")); + booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create")); + booksPermission.AddChild(BookStorePermissions.Books.Edit, L("Permission:Books.Edit")); + booksPermission.AddChild(BookStorePermissions.Books.Delete, L("Permission:Books.Delete")); + } + + private static LocalizableString L(string name) + { + return LocalizableString.Create(name); } } ```` @@ -130,7 +125,7 @@ Once you define the permissions, you can see them on the **permission management Go to the *Administration -> Identity -> Roles* page, select *Permissions* action for the admin role to open the permission management modal: - + Grant the permissions you want and save the modal. @@ -151,26 +146,25 @@ using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; -namespace Acme.BookStore.Books +namespace Acme.BookStore.Books; + +public class BookAppService : + CrudAppService< + Book, //The Book entity + BookDto, //Used to show books + Guid, //Primary key of the book entity + PagedAndSortedResultRequestDto, //Used for paging/sorting + CreateUpdateBookDto>, //Used to create/update a book + IBookAppService //implement the IBookAppService { - public class BookAppService : - CrudAppService< - Book, //The Book entity - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting - CreateUpdateBookDto>, //Used to create/update a book - IBookAppService //implement the IBookAppService + public BookAppService(IRepository repository) + : base(repository) { - public BookAppService(IRepository repository) - : base(repository) - { - GetPolicyName = BookStorePermissions.Books.Default; - GetListPolicyName = BookStorePermissions.Books.Default; - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Delete; - } + GetPolicyName = BookStorePermissions.Books.Default; + GetListPolicyName = BookStorePermissions.Books.Default; + CreatePolicyName = BookStorePermissions.Books.Create; + UpdatePolicyName = BookStorePermissions.Books.Edit; + DeletePolicyName = BookStorePermissions.Books.Delete; } } ```` @@ -202,7 +196,7 @@ Now, unauthorized users are redirected to the **login page**. The book management page has a *New Book* button that should be invisible if the current user has no *Book Creation* permission. - + Open the `Pages/Books/Index.cshtml` file and change the content as shown below: @@ -251,7 +245,7 @@ Open the `Pages/Books/Index.cshtml` file and change the content as shown below: Books table in the book management page has an actions button for each row. The actions button includes *Edit* and *Delete* actions: - + We should hide an action if the current user has not granted for the related permission. Datatables row actions has a `visible` option that can be set to `false` to hide the action item. @@ -301,82 +295,22 @@ context.Menu.AddItem( And replace this code block with the following: ````csharp -var bookStoreMenu = new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" +context.Menu.AddItem( + new ApplicationMenuItem( + "BooksStore", + l["Menu:BookStore"], + icon: "fa fa-book" + ).AddItem( + new ApplicationMenuItem( + "BooksStore.Books", + l["Menu:Books"], + url: "/Books" + ).RequirePermissions(BookStorePermissions.Books.Default) // Check the permission! + ) ); - -context.Menu.AddItem(bookStoreMenu); - -//CHECK the PERMISSION -if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) -{ - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/Books" - )); -} ```` -You also need to add `async` keyword to the `ConfigureMenuAsync` method and re-arrange the return values. The final `BookStoreMenuContributor` class should be the following: - -````csharp -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; -using Acme.BookStore.Localization; -using Acme.BookStore.MultiTenancy; -using Acme.BookStore.Permissions; -using Volo.Abp.TenantManagement.Web.Navigation; -using Volo.Abp.UI.Navigation; - -namespace Acme.BookStore.Web.Menus -{ - public class BookStoreMenuContributor : IMenuContributor - { - public async Task ConfigureMenuAsync(MenuConfigurationContext context) - { - if (context.Menu.Name == StandardMenus.Main) - { - await ConfigureMainMenuAsync(context); - } - } - - private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) - { - if (!MultiTenancyConsts.IsEnabled) - { - var administration = context.Menu.GetAdministration(); - administration.TryRemoveMenuItem(TenantManagementMenuNames.GroupName); - } - - var l = context.GetLocalizer(); - - context.Menu.Items.Insert(0, new ApplicationMenuItem("BookStore.Home", l["Menu:Home"], "~/")); - - var bookStoreMenu = new ApplicationMenuItem( - "BooksStore", - l["Menu:BookStore"], - icon: "fa fa-book" - ); - - context.Menu.AddItem(bookStoreMenu); - - //CHECK the PERMISSION - if (await context.IsGrantedAsync(BookStorePermissions.Books.Default)) - { - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Books", - l["Menu:Books"], - url: "/Books" - )); - } - } - } -} -```` +We've only added the `.RequirePermissions(BookStorePermissions.Books.Default)` extension method call for the inner menu item. {{else if UI == "NG"}} @@ -429,7 +363,7 @@ Open the `/src/app/book/book.component.html` file and replace the create button ````html - + {%{{{ '::NewBook' | abpLocalization }}}%} ```` @@ -440,7 +374,7 @@ Open the `/src/app/book/book.component.html` file and replace the create button Books table in the book management page has an actions button for each row. The actions button includes *Edit* and *Delete* actions: - + We should hide an action if the current user has not granted for the related permission. diff --git a/docs/en/Tutorials/Part-6.md b/docs/en/Tutorials/Part-6.md index 7d7c4e2d94..40b334808c 100644 --- a/docs/en/Tutorials/Part-6.md +++ b/docs/en/Tutorials/Part-6.md @@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer * [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) -> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` +> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). ## Introduction @@ -63,45 +60,44 @@ using JetBrains.Annotations; using Volo.Abp; using Volo.Abp.Domain.Entities.Auditing; -namespace Acme.BookStore.Authors +namespace Acme.BookStore.Authors; + +public class Author : FullAuditedAggregateRoot { - public class Author : FullAuditedAggregateRoot - { - public string Name { get; private set; } - public DateTime BirthDate { get; set; } - public string ShortBio { get; set; } + public string Name { get; private set; } + public DateTime BirthDate { get; set; } + public string ShortBio { get; set; } - private Author() - { - /* This constructor is for deserialization / ORM purpose */ - } + private Author() + { + /* This constructor is for deserialization / ORM purpose */ + } - internal Author( - Guid id, - [NotNull] string name, - DateTime birthDate, - [CanBeNull] string shortBio = null) - : base(id) - { - SetName(name); - BirthDate = birthDate; - ShortBio = shortBio; - } + internal Author( + Guid id, + [NotNull] string name, + DateTime birthDate, + [CanBeNull] string shortBio = null) + : base(id) + { + SetName(name); + BirthDate = birthDate; + ShortBio = shortBio; + } - internal Author ChangeName([NotNull] string name) - { - SetName(name); - return this; - } + internal Author ChangeName([NotNull] string name) + { + SetName(name); + return this; + } - private void SetName([NotNull] string name) - { - Name = Check.NotNullOrWhiteSpace( - name, - nameof(name), - maxLength: AuthorConsts.MaxNameLength - ); - } + private void SetName([NotNull] string name) + { + Name = Check.NotNullOrWhiteSpace( + name, + nameof(name), + maxLength: AuthorConsts.MaxNameLength + ); } } ```` @@ -116,13 +112,13 @@ namespace Acme.BookStore.Authors `AuthorConsts` is a simple class that is located under the `Authors` namespace (folder) of the `Acme.BookStore.Domain.Shared` project: ````csharp -namespace Acme.BookStore.Authors +namespace Acme.BookStore.Authors; + +public static class AuthorConsts { - public static class AuthorConsts - { - public const int MaxNameLength = 64; - } + public const int MaxNameLength = 64; } + ```` Created this class inside the `Acme.BookStore.Domain.Shared` project since we will re-use it on the [Data Transfer Objects](../Data-Transfer-Objects.md) (DTOs) later. @@ -138,53 +134,52 @@ using JetBrains.Annotations; using Volo.Abp; using Volo.Abp.Domain.Services; -namespace Acme.BookStore.Authors +namespace Acme.BookStore.Authors; + +public class AuthorManager : DomainService { - public class AuthorManager : DomainService + private readonly IAuthorRepository _authorRepository; + + public AuthorManager(IAuthorRepository authorRepository) { - private readonly IAuthorRepository _authorRepository; + _authorRepository = authorRepository; + } - public AuthorManager(IAuthorRepository authorRepository) - { - _authorRepository = authorRepository; - } + public async Task CreateAsync( + [NotNull] string name, + DateTime birthDate, + [CanBeNull] string shortBio = null) + { + Check.NotNullOrWhiteSpace(name, nameof(name)); - public async Task CreateAsync( - [NotNull] string name, - DateTime birthDate, - [CanBeNull] string shortBio = null) + var existingAuthor = await _authorRepository.FindByNameAsync(name); + if (existingAuthor != null) { - Check.NotNullOrWhiteSpace(name, nameof(name)); - - var existingAuthor = await _authorRepository.FindByNameAsync(name); - if (existingAuthor != null) - { - throw new AuthorAlreadyExistsException(name); - } - - return new Author( - GuidGenerator.Create(), - name, - birthDate, - shortBio - ); + throw new AuthorAlreadyExistsException(name); } - public async Task ChangeNameAsync( - [NotNull] Author author, - [NotNull] string newName) - { - Check.NotNull(author, nameof(author)); - Check.NotNullOrWhiteSpace(newName, nameof(newName)); + return new Author( + GuidGenerator.Create(), + name, + birthDate, + shortBio + ); + } - var existingAuthor = await _authorRepository.FindByNameAsync(newName); - if (existingAuthor != null && existingAuthor.Id != author.Id) - { - throw new AuthorAlreadyExistsException(newName); - } + public async Task ChangeNameAsync( + [NotNull] Author author, + [NotNull] string newName) + { + Check.NotNull(author, nameof(author)); + Check.NotNullOrWhiteSpace(newName, nameof(newName)); - author.ChangeName(newName); + var existingAuthor = await _authorRepository.FindByNameAsync(newName); + if (existingAuthor != null && existingAuthor.Id != author.Id) + { + throw new AuthorAlreadyExistsException(newName); } + + author.ChangeName(newName); } } ```` @@ -198,15 +193,14 @@ Both methods checks if there is already an author with the given name and throws ````csharp using Volo.Abp; -namespace Acme.BookStore.Authors +namespace Acme.BookStore.Authors; + +public class AuthorAlreadyExistsException : BusinessException { - public class AuthorAlreadyExistsException : BusinessException + public AuthorAlreadyExistsException(string name) + : base(BookStoreDomainErrorCodes.AuthorAlreadyExists) { - public AuthorAlreadyExistsException(string name) - : base(BookStoreDomainErrorCodes.AuthorAlreadyExists) - { - WithData("name", name); - } + WithData("name", name); } } ```` @@ -216,12 +210,11 @@ namespace Acme.BookStore.Authors Open the `BookStoreDomainErrorCodes` in the `Acme.BookStore.Domain.Shared` project and change as shown below: ````csharp -namespace Acme.BookStore +namespace Acme.BookStore; + +public static class BookStoreDomainErrorCodes { - public static class BookStoreDomainErrorCodes - { - public const string AuthorAlreadyExists = "BookStore:00001"; - } + public const string AuthorAlreadyExists = "BookStore:00001"; } ```` @@ -243,19 +236,18 @@ using System.Collections.Generic; using System.Threading.Tasks; using Volo.Abp.Domain.Repositories; -namespace Acme.BookStore.Authors -{ - public interface IAuthorRepository : IRepository - { - Task FindByNameAsync(string name); +namespace Acme.BookStore.Authors; - Task> GetListAsync( - int skipCount, - int maxResultCount, - string sorting, - string filter = null - ); - } +public interface IAuthorRepository : IRepository +{ + Task FindByNameAsync(string name); + + Task> GetListAsync( + int skipCount, + int maxResultCount, + string sorting, + string filter = null + ); } ```` @@ -265,7 +257,7 @@ namespace Acme.BookStore.Authors We will implement this repository in the next part. -> Both of these methods might **seem unnecessary** since the standard repositories already `IQueryable` and you can directly use them instead of defining such custom methods. You're right and do it like in a real application. However, for this **"learning" tutorial**, it is useful to explain how to create custom repository methods when you really need it. +> Both of these methods might **seem unnecessary** since the standard repositories already provide generic querying methods and you can easily use them instead of defining such custom methods. You're right and do it like in a real application. However, for this **"learning" tutorial**, it is useful to explain how to create custom repository methods when you really need it. ## Conclusion diff --git a/docs/en/Tutorials/Part-7.md b/docs/en/Tutorials/Part-7.md index 27d46bd4ed..82f01bf963 100644 --- a/docs/en/Tutorials/Part-7.md +++ b/docs/en/Tutorials/Part-7.md @@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer * [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) -> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` +> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). ## Introduction @@ -93,7 +90,7 @@ You can apply changes to the database using the following command, in the same c dotnet ef database update ```` -> If you are using Visual Studio, you may want to use `Add-Migration Added_Authors -c BookStoreDbContext` and `Update-Database -Context BookStoreDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC. +> If you are using Visual Studio, you may want to use the `Add-Migration Created_Book_Entity` and `Update-Database` commands in the *Package Manager Console (PMC)*. In this case, ensure that `Acme.BookStore.EntityFrameworkCore` is the startup project in Visual Studio and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC. {{else if DB=="Mongo"}} @@ -124,41 +121,40 @@ using Microsoft.EntityFrameworkCore; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; -namespace Acme.BookStore.Authors +namespace Acme.BookStore.Authors; + +public class EfCoreAuthorRepository + : EfCoreRepository, + IAuthorRepository { - public class EfCoreAuthorRepository - : EfCoreRepository, - IAuthorRepository + public EfCoreAuthorRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task FindByNameAsync(string name) { - public EfCoreAuthorRepository( - IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } - - public async Task FindByNameAsync(string name) - { - var dbSet = await GetDbSetAsync(); - return await dbSet.FirstOrDefaultAsync(author => author.Name == name); - } - - public async Task> GetListAsync( - int skipCount, - int maxResultCount, - string sorting, - string filter = null) - { - var dbSet = await GetDbSetAsync(); - return await dbSet - .WhereIf( - !filter.IsNullOrWhiteSpace(), - author => author.Name.Contains(filter) - ) - .OrderBy(sorting) - .Skip(skipCount) - .Take(maxResultCount) - .ToListAsync(); - } + var dbSet = await GetDbSetAsync(); + return await dbSet.FirstOrDefaultAsync(author => author.Name == name); + } + + public async Task> GetListAsync( + int skipCount, + int maxResultCount, + string sorting, + string filter = null) + { + var dbSet = await GetDbSetAsync(); + return await dbSet + .WhereIf( + !filter.IsNullOrWhiteSpace(), + author => author.Name.Contains(filter) + ) + .OrderBy(sorting) + .Skip(skipCount) + .Take(maxResultCount) + .ToListAsync(); } } ```` @@ -185,42 +181,41 @@ using MongoDB.Driver.Linq; using Volo.Abp.Domain.Repositories.MongoDB; using Volo.Abp.MongoDB; -namespace Acme.BookStore.Authors +namespace Acme.BookStore.Authors; + +public class MongoDbAuthorRepository + : MongoDbRepository, + IAuthorRepository { - public class MongoDbAuthorRepository - : MongoDbRepository, - IAuthorRepository + public MongoDbAuthorRepository( + IMongoDbContextProvider dbContextProvider + ) : base(dbContextProvider) { - public MongoDbAuthorRepository( - IMongoDbContextProvider dbContextProvider - ) : base(dbContextProvider) - { - } - - public async Task FindByNameAsync(string name) - { - var queryable = await GetMongoQueryableAsync(); - return await queryable.FirstOrDefaultAsync(author => author.Name == name); - } - - public async Task> GetListAsync( - int skipCount, - int maxResultCount, - string sorting, - string filter = null) - { - var queryable = await GetMongoQueryableAsync(); - return await queryable - .WhereIf>( - !filter.IsNullOrWhiteSpace(), - author => author.Name.Contains(filter) - ) - .OrderBy(sorting) - .As>() - .Skip(skipCount) - .Take(maxResultCount) - .ToListAsync(); - } + } + + public async Task FindByNameAsync(string name) + { + var queryable = await GetMongoQueryableAsync(); + return await queryable.FirstOrDefaultAsync(author => author.Name == name); + } + + public async Task> GetListAsync( + int skipCount, + int maxResultCount, + string sorting, + string filter = null) + { + var queryable = await GetMongoQueryableAsync(); + return await queryable + .WhereIf>( + !filter.IsNullOrWhiteSpace(), + author => author.Name.Contains(filter) + ) + .OrderBy(sorting) + .As>() + .Skip(skipCount) + .Take(maxResultCount) + .ToListAsync(); } } ``` diff --git a/docs/en/Tutorials/Part-8.md b/docs/en/Tutorials/Part-8.md index d8dd4bc5b1..24dafffdb9 100644 --- a/docs/en/Tutorials/Part-8.md +++ b/docs/en/Tutorials/Part-8.md @@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer * [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) -> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` +> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). ## Introduction @@ -53,20 +50,19 @@ using System.Threading.Tasks; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; -namespace Acme.BookStore.Authors +namespace Acme.BookStore.Authors; + +public interface IAuthorAppService : IApplicationService { - public interface IAuthorAppService : IApplicationService - { - Task GetAsync(Guid id); + Task GetAsync(Guid id); - Task> GetListAsync(GetAuthorListDto input); + Task> GetListAsync(GetAuthorListDto input); - Task CreateAsync(CreateAuthorDto input); + Task CreateAsync(CreateAuthorDto input); - Task UpdateAsync(Guid id, UpdateAuthorDto input); + Task UpdateAsync(Guid id, UpdateAuthorDto input); - Task DeleteAsync(Guid id); - } + Task DeleteAsync(Guid id); } ```` @@ -83,16 +79,15 @@ This interface is using the DTOs defined below (create them for your project). using System; using Volo.Abp.Application.Dtos; -namespace Acme.BookStore.Authors +namespace Acme.BookStore.Authors; + +public class AuthorDto : EntityDto { - public class AuthorDto : EntityDto - { - public string Name { get; set; } + public string Name { get; set; } - public DateTime BirthDate { get; set; } + public DateTime BirthDate { get; set; } - public string ShortBio { get; set; } - } + public string ShortBio { get; set; } } ```` @@ -103,12 +98,11 @@ namespace Acme.BookStore.Authors ````csharp using Volo.Abp.Application.Dtos; -namespace Acme.BookStore.Authors +namespace Acme.BookStore.Authors; + +public class GetAuthorListDto : PagedAndSortedResultRequestDto { - public class GetAuthorListDto : PagedAndSortedResultRequestDto - { - public string Filter { get; set; } - } + public string Filter { get; set; } } ```` @@ -123,19 +117,18 @@ namespace Acme.BookStore.Authors using System; using System.ComponentModel.DataAnnotations; -namespace Acme.BookStore.Authors -{ - public class CreateAuthorDto - { - [Required] - [StringLength(AuthorConsts.MaxNameLength)] - public string Name { get; set; } +namespace Acme.BookStore.Authors; - [Required] - public DateTime BirthDate { get; set; } - - public string ShortBio { get; set; } - } +public class CreateAuthorDto +{ + [Required] + [StringLength(AuthorConsts.MaxNameLength)] + public string Name { get; set; } + + [Required] + public DateTime BirthDate { get; set; } + + public string ShortBio { get; set; } } ```` @@ -147,19 +140,18 @@ Data annotation attributes can be used to validate the DTO. See the [validation using System; using System.ComponentModel.DataAnnotations; -namespace Acme.BookStore.Authors -{ - public class UpdateAuthorDto - { - [Required] - [StringLength(AuthorConsts.MaxNameLength)] - public string Name { get; set; } +namespace Acme.BookStore.Authors; - [Required] - public DateTime BirthDate { get; set; } - - public string ShortBio { get; set; } - } +public class UpdateAuthorDto +{ + [Required] + [StringLength(AuthorConsts.MaxNameLength)] + public string Name { get; set; } + + [Required] + public DateTime BirthDate { get; set; } + + public string ShortBio { get; set; } } ```` @@ -179,24 +171,23 @@ using Microsoft.AspNetCore.Authorization; using Volo.Abp.Application.Dtos; using Volo.Abp.Domain.Repositories; -namespace Acme.BookStore.Authors -{ - [Authorize(BookStorePermissions.Authors.Default)] - public class AuthorAppService : BookStoreAppService, IAuthorAppService - { - private readonly IAuthorRepository _authorRepository; - private readonly AuthorManager _authorManager; +namespace Acme.BookStore.Authors; - public AuthorAppService( - IAuthorRepository authorRepository, - AuthorManager authorManager) - { - _authorRepository = authorRepository; - _authorManager = authorManager; - } +[Authorize(BookStorePermissions.Authors.Default)] +public class AuthorAppService : BookStoreAppService, IAuthorAppService +{ + private readonly IAuthorRepository _authorRepository; + private readonly AuthorManager _authorManager; - //...SERVICE METHODS WILL COME HERE... + public AuthorAppService( + IAuthorRepository authorRepository, + AuthorManager authorManager) + { + _authorRepository = authorRepository; + _authorManager = authorManager; } + + //...SERVICE METHODS WILL COME HERE... } ```` @@ -330,28 +321,27 @@ You can't compile the code since it is expecting some constants declared in the Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project (in the `Permissions` folder) and change the content as shown below: ````csharp -namespace Acme.BookStore.Permissions +namespace Acme.BookStore.Permissions; + +public static class BookStorePermissions { - public static class BookStorePermissions - { - public const string GroupName = "BookStore"; + public const string GroupName = "BookStore"; - public static class Books - { - public const string Default = GroupName + ".Books"; - public const string Create = Default + ".Create"; - public const string Edit = Default + ".Edit"; - public const string Delete = Default + ".Delete"; - } - - // *** ADDED a NEW NESTED CLASS *** - public static class Authors - { - public const string Default = GroupName + ".Authors"; - public const string Create = Default + ".Create"; - public const string Edit = Default + ".Edit"; - public const string Delete = Default + ".Delete"; - } + public static class Books + { + public const string Default = GroupName + ".Books"; + public const string Create = Default + ".Create"; + public const string Edit = Default + ".Edit"; + public const string Delete = Default + ".Delete"; + } + + // *** ADDED a NEW NESTED CLASS *** + public static class Authors + { + public const string Default = GroupName + ".Authors"; + public const string Create = Default + ".Create"; + public const string Edit = Default + ".Edit"; + public const string Delete = Default + ".Delete"; } } ```` @@ -361,13 +351,10 @@ Then open the `BookStorePermissionDefinitionProvider` in the same project and ad ````csharp var authorsPermission = bookStoreGroup.AddPermission( BookStorePermissions.Authors.Default, L("Permission:Authors")); - authorsPermission.AddChild( BookStorePermissions.Authors.Create, L("Permission:Authors.Create")); - authorsPermission.AddChild( BookStorePermissions.Authors.Edit, L("Permission:Authors.Edit")); - authorsPermission.AddChild( BookStorePermissions.Authors.Delete, L("Permission:Authors.Delete")); ```` @@ -406,72 +393,71 @@ using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; -namespace Acme.BookStore +namespace Acme.BookStore; + +public class BookStoreDataSeederContributor + : IDataSeedContributor, ITransientDependency { - public class BookStoreDataSeederContributor - : IDataSeedContributor, ITransientDependency + private readonly IRepository _bookRepository; + private readonly IAuthorRepository _authorRepository; + private readonly AuthorManager _authorManager; + + public BookStoreDataSeederContributor( + IRepository bookRepository, + IAuthorRepository authorRepository, + AuthorManager authorManager) { - private readonly IRepository _bookRepository; - private readonly IAuthorRepository _authorRepository; - private readonly AuthorManager _authorManager; - - public BookStoreDataSeederContributor( - IRepository bookRepository, - IAuthorRepository authorRepository, - AuthorManager authorManager) + _bookRepository = bookRepository; + _authorRepository = authorRepository; + _authorManager = authorManager; + } + + public async Task SeedAsync(DataSeedContext context) + { + if (await _bookRepository.GetCountAsync() <= 0) { - _bookRepository = bookRepository; - _authorRepository = authorRepository; - _authorManager = authorManager; + await _bookRepository.InsertAsync( + new Book + { + Name = "1984", + Type = BookType.Dystopia, + PublishDate = new DateTime(1949, 6, 8), + Price = 19.84f + }, + autoSave: true + ); + + await _bookRepository.InsertAsync( + new Book + { + Name = "The Hitchhiker's Guide to the Galaxy", + Type = BookType.ScienceFiction, + PublishDate = new DateTime(1995, 9, 27), + Price = 42.0f + }, + autoSave: true + ); } - public async Task SeedAsync(DataSeedContext context) - { - if (await _bookRepository.GetCountAsync() <= 0) - { - await _bookRepository.InsertAsync( - new Book - { - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - }, - autoSave: true - ); - - await _bookRepository.InsertAsync( - new Book - { - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - }, - autoSave: true - ); - } + // ADDED SEED DATA FOR AUTHORS - // ADDED SEED DATA FOR AUTHORS + if (await _authorRepository.GetCountAsync() <= 0) + { + await _authorRepository.InsertAsync( + await _authorManager.CreateAsync( + "George Orwell", + new DateTime(1903, 06, 25), + "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." + ) + ); - if (await _authorRepository.GetCountAsync() <= 0) - { - await _authorRepository.InsertAsync( - await _authorManager.CreateAsync( - "George Orwell", - new DateTime(1903, 06, 25), - "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." - ) - ); - - await _authorRepository.InsertAsync( - await _authorManager.CreateAsync( - "Douglas Adams", - new DateTime(1952, 03, 11), - "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." - ) - ); - } + await _authorRepository.InsertAsync( + await _authorManager.CreateAsync( + "Douglas Adams", + new DateTime(1952, 03, 11), + "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." + ) + ); } } } @@ -497,73 +483,74 @@ using System.Threading.Tasks; using Shouldly; using Xunit; -namespace Acme.BookStore.Authors -{ {{if DB=="Mongo"}} - [Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} - public class AuthorAppService_Tests : BookStoreApplicationTestBase +namespace Acme.BookStore.Authors; + + {{if DB=="Mongo"}} +[Collection(BookStoreTestConsts.CollectionDefinitionName)] +{{end}} +public class AuthorAppService_Tests : BookStoreApplicationTestBase +{ + private readonly IAuthorAppService _authorAppService; + + public AuthorAppService_Tests() { - private readonly IAuthorAppService _authorAppService; + _authorAppService = GetRequiredService(); + } - public AuthorAppService_Tests() - { - _authorAppService = GetRequiredService(); - } + [Fact] + public async Task Should_Get_All_Authors_Without_Any_Filter() + { + var result = await _authorAppService.GetListAsync(new GetAuthorListDto()); - [Fact] - public async Task Should_Get_All_Authors_Without_Any_Filter() - { - var result = await _authorAppService.GetListAsync(new GetAuthorListDto()); + result.TotalCount.ShouldBeGreaterThanOrEqualTo(2); + result.Items.ShouldContain(author => author.Name == "George Orwell"); + result.Items.ShouldContain(author => author.Name == "Douglas Adams"); + } - result.TotalCount.ShouldBeGreaterThanOrEqualTo(2); - result.Items.ShouldContain(author => author.Name == "George Orwell"); - result.Items.ShouldContain(author => author.Name == "Douglas Adams"); - } + [Fact] + public async Task Should_Get_Filtered_Authors() + { + var result = await _authorAppService.GetListAsync( + new GetAuthorListDto {Filter = "George"}); - [Fact] - public async Task Should_Get_Filtered_Authors() - { - var result = await _authorAppService.GetListAsync( - new GetAuthorListDto {Filter = "George"}); + result.TotalCount.ShouldBeGreaterThanOrEqualTo(1); + result.Items.ShouldContain(author => author.Name == "George Orwell"); + result.Items.ShouldNotContain(author => author.Name == "Douglas Adams"); + } - result.TotalCount.ShouldBeGreaterThanOrEqualTo(1); - result.Items.ShouldContain(author => author.Name == "George Orwell"); - result.Items.ShouldNotContain(author => author.Name == "Douglas Adams"); - } + [Fact] + public async Task Should_Create_A_New_Author() + { + var authorDto = await _authorAppService.CreateAsync( + new CreateAuthorDto + { + Name = "Edward Bellamy", + BirthDate = new DateTime(1850, 05, 22), + ShortBio = "Edward Bellamy was an American author..." + } + ); + + authorDto.Id.ShouldNotBe(Guid.Empty); + authorDto.Name.ShouldBe("Edward Bellamy"); + } - [Fact] - public async Task Should_Create_A_New_Author() + [Fact] + public async Task Should_Not_Allow_To_Create_Duplicate_Author() + { + await Assert.ThrowsAsync(async () => { - var authorDto = await _authorAppService.CreateAsync( + await _authorAppService.CreateAsync( new CreateAuthorDto { - Name = "Edward Bellamy", - BirthDate = new DateTime(1850, 05, 22), - ShortBio = "Edward Bellamy was an American author..." + Name = "Douglas Adams", + BirthDate = DateTime.Now, + ShortBio = "..." } ); - - authorDto.Id.ShouldNotBe(Guid.Empty); - authorDto.Name.ShouldBe("Edward Bellamy"); - } - - [Fact] - public async Task Should_Not_Allow_To_Create_Duplicate_Author() - { - await Assert.ThrowsAsync(async () => - { - await _authorAppService.CreateAsync( - new CreateAuthorDto - { - Name = "Douglas Adams", - BirthDate = DateTime.Now, - ShortBio = "..." - } - ); - }); - } - - //TODO: Test other methods... + }); } + + //TODO: Test other methods... } ```` diff --git a/docs/en/Tutorials/Part-9.md b/docs/en/Tutorials/Part-9.md index 76d6d9c8d8..ba67e2d258 100644 --- a/docs/en/Tutorials/Part-9.md +++ b/docs/en/Tutorials/Part-9.md @@ -34,10 +34,7 @@ This tutorial has multiple versions based on your **UI** and **Database** prefer * [Blazor UI with EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore) * [Angular UI with MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) -> If you encounter the "filename too long" or "unzip error" on Windows, it's probably related to the Windows maximum file path limitation. Windows has a maximum file path limitation of 250 characters. To solve this, [enable the long path option in Windows 10](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later). - -> If you face long path errors related to Git, try the following command to enable long paths in Windows. See https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path -> `git config --system core.longpaths true` +> If you encounter the "filename too long" or "unzip" error on Windows, please see [this guide](../KB/Windows-Path-Too-Long-Fix.md). ## Introduction @@ -98,14 +95,13 @@ This is a simple page similar to the Books page we had created before. It import ````csharp using Microsoft.AspNetCore.Mvc.RazorPages; -namespace Acme.BookStore.Web.Pages.Authors +namespace Acme.BookStore.Web.Pages.Authors; + +public class IndexModel : PageModel { - public class IndexModel : PageModel + public void OnGet() { - public void OnGet() - { - } } } ```` @@ -223,28 +219,39 @@ Notice that we've added more keys. They will be used in the next sections. ### Add to the Main Menu -Open the `BookStoreMenuContributor.cs` in the `Menus` folder of the `Acme.BookStore.Web` project and add the following code in the end of the `ConfigureMainMenuAsync` method: +Open the `BookStoreMenuContributor.cs` in the `Menus` folder of the `Acme.BookStore.Web` project and add a new *Authors* menu item under the *Book Store* menu item. The following code (in the `ConfigureMainMenuAsync` method) shows the final code part: ````csharp -if (await context.IsGrantedAsync(BookStorePermissions.Authors.Default)) -{ - bookStoreMenu.AddItem(new ApplicationMenuItem( - "BooksStore.Authors", - l["Menu:Authors"], - url: "/Authors" - )); -} +context.Menu.AddItem( + new ApplicationMenuItem( + "BooksStore", + l["Menu:BookStore"], + icon: "fa fa-book" + ).AddItem( + new ApplicationMenuItem( + "BooksStore.Books", + l["Menu:Books"], + url: "/Books" + ).RequirePermissions(BookStorePermissions.Books.Default) + ).AddItem( // ADDED THE NEW "AUTHORS" MENU ITEM UNDER THE "BOOK STORE" MENU + new ApplicationMenuItem( + "BooksStore.Authors", + l["Menu:Authors"], + url: "/Authors" + ).RequirePermissions(BookStorePermissions.Books.Default) + ) +); ```` ### Run the Application Run and login to the application. **You can not see the menu item since you don't have permission yet.** Go to the `Identity/Roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**: - + As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**: - + The page is fully working except *New author* and *Actions/Edit* since we haven't implemented them yet. @@ -294,45 +301,44 @@ using Acme.BookStore.Authors; using Microsoft.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; -namespace Acme.BookStore.Web.Pages.Authors +namespace Acme.BookStore.Web.Pages.Authors; + +public class CreateModalModel : BookStorePageModel { - public class CreateModalModel : BookStorePageModel - { - [BindProperty] - public CreateAuthorViewModel Author { get; set; } + [BindProperty] + public CreateAuthorViewModel Author { get; set; } - private readonly IAuthorAppService _authorAppService; + private readonly IAuthorAppService _authorAppService; - public CreateModalModel(IAuthorAppService authorAppService) - { - _authorAppService = authorAppService; - } + public CreateModalModel(IAuthorAppService authorAppService) + { + _authorAppService = authorAppService; + } - public void OnGet() - { - Author = new CreateAuthorViewModel(); - } + public void OnGet() + { + Author = new CreateAuthorViewModel(); + } - public async Task OnPostAsync() - { - var dto = ObjectMapper.Map(Author); - await _authorAppService.CreateAsync(dto); - return NoContent(); - } + public async Task OnPostAsync() + { + var dto = ObjectMapper.Map(Author); + await _authorAppService.CreateAsync(dto); + return NoContent(); + } - public class CreateAuthorViewModel - { - [Required] - [StringLength(AuthorConsts.MaxNameLength)] - public string Name { get; set; } + public class CreateAuthorViewModel + { + [Required] + [StringLength(AuthorConsts.MaxNameLength)] + public string Name { get; set; } - [Required] - [DataType(DataType.Date)] - public DateTime BirthDate { get; set; } + [Required] + [DataType(DataType.Date)] + public DateTime BirthDate { get; set; } - [TextArea] - public string ShortBio { get; set; } - } + [TextArea] + public string ShortBio { get; set; } } } ``` @@ -351,25 +357,24 @@ using Acme.BookStore.Authors; // ADDED NAMESPACE IMPORT using Acme.BookStore.Books; using AutoMapper; -namespace Acme.BookStore.Web +namespace Acme.BookStore.Web; + +public class BookStoreWebAutoMapperProfile : Profile { - public class BookStoreWebAutoMapperProfile : Profile + public BookStoreWebAutoMapperProfile() { - public BookStoreWebAutoMapperProfile() - { - CreateMap(); + CreateMap(); - // ADD a NEW MAPPING - CreateMap(); - } + // ADD a NEW MAPPING + CreateMap(); } } ```` "New author" button will work as expected and open a new model when you run the application again: - + ## Edit Modal @@ -412,52 +417,51 @@ using Acme.BookStore.Authors; using Microsoft.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; -namespace Acme.BookStore.Web.Pages.Authors +namespace Acme.BookStore.Web.Pages.Authors; + +public class EditModalModel : BookStorePageModel { - public class EditModalModel : BookStorePageModel - { - [BindProperty] - public EditAuthorViewModel Author { get; set; } + [BindProperty] + public EditAuthorViewModel Author { get; set; } - private readonly IAuthorAppService _authorAppService; + private readonly IAuthorAppService _authorAppService; - public EditModalModel(IAuthorAppService authorAppService) - { - _authorAppService = authorAppService; - } + public EditModalModel(IAuthorAppService authorAppService) + { + _authorAppService = authorAppService; + } - public async Task OnGetAsync(Guid id) - { - var authorDto = await _authorAppService.GetAsync(id); - Author = ObjectMapper.Map(authorDto); - } + public async Task OnGetAsync(Guid id) + { + var authorDto = await _authorAppService.GetAsync(id); + Author = ObjectMapper.Map(authorDto); + } - public async Task OnPostAsync() - { - await _authorAppService.UpdateAsync( - Author.Id, - ObjectMapper.Map(Author) - ); + public async Task OnPostAsync() + { + await _authorAppService.UpdateAsync( + Author.Id, + ObjectMapper.Map(Author) + ); - return NoContent(); - } + return NoContent(); + } - public class EditAuthorViewModel - { - [HiddenInput] - public Guid Id { get; set; } + public class EditAuthorViewModel + { + [HiddenInput] + public Guid Id { get; set; } - [Required] - [StringLength(AuthorConsts.MaxNameLength)] - public string Name { get; set; } + [Required] + [StringLength(AuthorConsts.MaxNameLength)] + public string Name { get; set; } - [Required] - [DataType(DataType.Date)] - public DateTime BirthDate { get; set; } + [Required] + [DataType(DataType.Date)] + public DateTime BirthDate { get; set; } - [TextArea] - public string ShortBio { get; set; } - } + [TextArea] + public string ShortBio { get; set; } } } ``` @@ -474,22 +478,21 @@ using Acme.BookStore.Authors; using Acme.BookStore.Books; using AutoMapper; -namespace Acme.BookStore.Web +namespace Acme.BookStore.Web; + +public class BookStoreWebAutoMapperProfile : Profile { - public class BookStoreWebAutoMapperProfile : Profile + public BookStoreWebAutoMapperProfile() { - public BookStoreWebAutoMapperProfile() - { - CreateMap(); + CreateMap(); - CreateMap(); + CreateMap(); - // ADD THESE NEW MAPPINGS - CreateMap(); - CreateMap(); - } + // ADD THESE NEW MAPPINGS + CreateMap(); + CreateMap(); } } ``` @@ -725,7 +728,7 @@ Open the `/src/app/author/author.component.html` and replace the content as belo - + {%{{{ '::NewAuthor' | abpLocalization }}}%} @@ -747,7 +750,7 @@ Open the `/src/app/author/author.component.html` and replace the content as belo aria-haspopup="true" ngbDropdownToggle > - {%{{{ '::Actions' | abpLocalization }}}%} + {%{{{ '::Actions' | abpLocalization }}}%} @@ -782,7 +785,7 @@ Open the `/src/app/author/author.component.html` and replace the content as belo - + Birth date * LH - - + + @L["Authors"] - - + @if (CanCreateAuthor) { } - @@ -1046,142 +1047,141 @@ using Blazorise.DataGrid; using Microsoft.AspNetCore.Authorization; using Volo.Abp.Application.Dtos; -namespace Acme.BookStore.Blazor.Pages +namespace Acme.BookStore.Blazor.Pages; + +public partial class Authors { - public partial class Authors - { - private IReadOnlyList AuthorList { get; set; } + private IReadOnlyList AuthorList { get; set; } - private int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount; - private int CurrentPage { get; set; } - private string CurrentSorting { get; set; } - private int TotalCount { get; set; } + private int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount; + private int CurrentPage { get; set; } + private string CurrentSorting { get; set; } + private int TotalCount { get; set; } - private bool CanCreateAuthor { get; set; } - private bool CanEditAuthor { get; set; } - private bool CanDeleteAuthor { get; set; } + private bool CanCreateAuthor { get; set; } + private bool CanEditAuthor { get; set; } + private bool CanDeleteAuthor { get; set; } - private CreateAuthorDto NewAuthor { get; set; } + private CreateAuthorDto NewAuthor { get; set; } - private Guid EditingAuthorId { get; set; } - private UpdateAuthorDto EditingAuthor { get; set; } + private Guid EditingAuthorId { get; set; } + private UpdateAuthorDto EditingAuthor { get; set; } - private Modal CreateAuthorModal { get; set; } - private Modal EditAuthorModal { get; set; } + private Modal CreateAuthorModal { get; set; } + private Modal EditAuthorModal { get; set; } - private Validations CreateValidationsRef; - - private Validations EditValidationsRef; - - public Authors() - { - NewAuthor = new CreateAuthorDto(); - EditingAuthor = new UpdateAuthorDto(); - } + private Validations CreateValidationsRef; + + private Validations EditValidationsRef; + + public Authors() + { + NewAuthor = new CreateAuthorDto(); + EditingAuthor = new UpdateAuthorDto(); + } - protected override async Task OnInitializedAsync() - { - await SetPermissionsAsync(); - await GetAuthorsAsync(); - } + protected override async Task OnInitializedAsync() + { + await SetPermissionsAsync(); + await GetAuthorsAsync(); + } - private async Task SetPermissionsAsync() - { - CanCreateAuthor = await AuthorizationService - .IsGrantedAsync(BookStorePermissions.Authors.Create); + private async Task SetPermissionsAsync() + { + CanCreateAuthor = await AuthorizationService + .IsGrantedAsync(BookStorePermissions.Authors.Create); - CanEditAuthor = await AuthorizationService - .IsGrantedAsync(BookStorePermissions.Authors.Edit); + CanEditAuthor = await AuthorizationService + .IsGrantedAsync(BookStorePermissions.Authors.Edit); - CanDeleteAuthor = await AuthorizationService - .IsGrantedAsync(BookStorePermissions.Authors.Delete); - } + CanDeleteAuthor = await AuthorizationService + .IsGrantedAsync(BookStorePermissions.Authors.Delete); + } - private async Task GetAuthorsAsync() - { - var result = await AuthorAppService.GetListAsync( - new GetAuthorListDto - { - MaxResultCount = PageSize, - SkipCount = CurrentPage * PageSize, - Sorting = CurrentSorting - } - ); + private async Task GetAuthorsAsync() + { + var result = await AuthorAppService.GetListAsync( + new GetAuthorListDto + { + MaxResultCount = PageSize, + SkipCount = CurrentPage * PageSize, + Sorting = CurrentSorting + } + ); - AuthorList = result.Items; - TotalCount = (int)result.TotalCount; - } + AuthorList = result.Items; + TotalCount = (int)result.TotalCount; + } - private async Task OnDataGridReadAsync(DataGridReadDataEventArgs e) - { - CurrentSorting = e.Columns - .Where(c => c.SortDirection != SortDirection.Default) - .Select(c => c.Field + (c.SortDirection == SortDirection.Descending ? " DESC" : "")) - .JoinAsString(","); - CurrentPage = e.Page - 1; + private async Task OnDataGridReadAsync(DataGridReadDataEventArgs e) + { + CurrentSorting = e.Columns + .Where(c => c.SortDirection != SortDirection.Default) + .Select(c => c.Field + (c.SortDirection == SortDirection.Descending ? " DESC" : "")) + .JoinAsString(","); + CurrentPage = e.Page - 1; - await GetAuthorsAsync(); + await GetAuthorsAsync(); - await InvokeAsync(StateHasChanged); - } + await InvokeAsync(StateHasChanged); + } - private void OpenCreateAuthorModal() - { - CreateValidationsRef.ClearAll(); - - NewAuthor = new CreateAuthorDto(); - CreateAuthorModal.Show(); - } + private void OpenCreateAuthorModal() + { + CreateValidationsRef.ClearAll(); + + NewAuthor = new CreateAuthorDto(); + CreateAuthorModal.Show(); + } - private void CloseCreateAuthorModal() - { - CreateAuthorModal.Hide(); - } + private void CloseCreateAuthorModal() + { + CreateAuthorModal.Hide(); + } - private void OpenEditAuthorModal(AuthorDto author) - { - EditValidationsRef.ClearAll(); - - EditingAuthorId = author.Id; - EditingAuthor = ObjectMapper.Map(author); - EditAuthorModal.Show(); - } + private void OpenEditAuthorModal(AuthorDto author) + { + EditValidationsRef.ClearAll(); + + EditingAuthorId = author.Id; + EditingAuthor = ObjectMapper.Map(author); + EditAuthorModal.Show(); + } - private async Task DeleteAuthorAsync(AuthorDto author) + private async Task DeleteAuthorAsync(AuthorDto author) + { + var confirmMessage = L["AuthorDeletionConfirmationMessage", author.Name]; + if (!await Message.Confirm(confirmMessage)) { - var confirmMessage = L["AuthorDeletionConfirmationMessage", author.Name]; - if (!await Message.Confirm(confirmMessage)) - { - return; - } - - await AuthorAppService.DeleteAsync(author.Id); - await GetAuthorsAsync(); + return; } - private void CloseEditAuthorModal() - { - EditAuthorModal.Hide(); - } + await AuthorAppService.DeleteAsync(author.Id); + await GetAuthorsAsync(); + } + + private void CloseEditAuthorModal() + { + EditAuthorModal.Hide(); + } - private async Task CreateAuthorAsync() + private async Task CreateAuthorAsync() + { + if (await CreateValidationsRef.ValidateAll()) { - if (await CreateValidationsRef.ValidateAll()) - { - await AuthorAppService.CreateAsync(NewAuthor); - await GetAuthorsAsync(); - CreateAuthorModal.Hide(); - } + await AuthorAppService.CreateAsync(NewAuthor); + await GetAuthorsAsync(); + CreateAuthorModal.Hide(); } + } - private async Task UpdateAuthorAsync() + private async Task UpdateAuthorAsync() + { + if (await EditValidationsRef.ValidateAll()) { - if (await EditValidationsRef.ValidateAll()) - { - await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor); - await GetAuthorsAsync(); - EditAuthorModal.Hide(); - } + await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor); + await GetAuthorsAsync(); + EditAuthorModal.Hide(); } } } @@ -1232,11 +1232,11 @@ We should complete the localizations we've used above. Open the `en.json` file u Run and login to the application. **If you don't see the Authors menu item under the Book Store menu, that means you don't have the permission yet.** Go to the `identity/roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**: - + As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**: - + That's all! This is a fully working CRUD page, you can create, edit and delete the authors. diff --git a/docs/en/Tutorials/Todo/Index.md b/docs/en/Tutorials/Todo/Index.md index c51a0206f3..20465b7820 100644 --- a/docs/en/Tutorials/Todo/Index.md +++ b/docs/en/Tutorials/Todo/Index.md @@ -14,6 +14,42 @@ This is a single-part quick-start tutorial to build a simple todo application wi You can find the source code of the completed application [here](https://github.com/abpframework/abp-samples/tree/master/TodoApp). +This documentation has a video tutorial on **YouTube**!! You can watch it here: + +{{if UI=="MVC" && DB =="EF"}} + + + +{{else if UI=="Blazor" && DB=="EF"}} + + + +{{else if UI=="BlazorServer" && DB=="EF"}} + + + +{{else if UI=="NG" && DB=="EF"}} + + + +{{else if UI=="MVC" && DB=="Mongo"}} + + + +{{else if UI=="BlazorServer" && DB=="Mongo"}} + + + +{{else if UI=="Blazor" && DB=="Mongo"}} + + + +{{else if UI=="NG" && DB=="Mongo"}} + + + +{{end}} + ## Pre-Requirements * An IDE (e.g. [Visual Studio](https://visualstudio.microsoft.com/vs/)) that supports [.NET 7.0+](https://dotnet.microsoft.com/download/dotnet) development. diff --git a/docs/en/Tutorials/Todo/Single-Layer/todo-single-layer-ui-initial.png b/docs/en/Tutorials/Todo/Single-Layer/todo-single-layer-ui-initial.png index 2393505147..a1d113f574 100644 Binary files a/docs/en/Tutorials/Todo/Single-Layer/todo-single-layer-ui-initial.png and b/docs/en/Tutorials/Todo/Single-Layer/todo-single-layer-ui-initial.png differ diff --git a/docs/en/Tutorials/Todo/todo-list.png b/docs/en/Tutorials/Todo/todo-list.png index e2eaf8bc46..6394dd8f29 100644 Binary files a/docs/en/Tutorials/Todo/todo-list.png and b/docs/en/Tutorials/Todo/todo-list.png differ diff --git a/docs/en/Tutorials/Todo/todo-ui-initial.png b/docs/en/Tutorials/Todo/todo-ui-initial.png index 44be4ceaf6..9cc1b2e2be 100644 Binary files a/docs/en/Tutorials/Todo/todo-ui-initial.png and b/docs/en/Tutorials/Todo/todo-ui-initial.png differ diff --git a/docs/en/Tutorials/images/blazor-add-book-button-2.png b/docs/en/Tutorials/images/blazor-add-book-button-2.png new file mode 100644 index 0000000000..e6760b7405 Binary files /dev/null and b/docs/en/Tutorials/images/blazor-add-book-button-2.png differ diff --git a/docs/en/Tutorials/images/blazor-bookstore-book-list-2.png b/docs/en/Tutorials/images/blazor-bookstore-book-list-2.png new file mode 100644 index 0000000000..2dda8471dc Binary files /dev/null and b/docs/en/Tutorials/images/blazor-bookstore-book-list-2.png differ diff --git a/docs/en/Tutorials/images/blazor-bookstore-book-list-with-authors-2.png b/docs/en/Tutorials/images/blazor-bookstore-book-list-with-authors-2.png new file mode 100644 index 0000000000..3bc399ca17 Binary files /dev/null and b/docs/en/Tutorials/images/blazor-bookstore-book-list-with-authors-2.png differ diff --git a/docs/en/Tutorials/images/blazor-delete-book-action-2.png b/docs/en/Tutorials/images/blazor-delete-book-action-2.png new file mode 100644 index 0000000000..5e1e144efa Binary files /dev/null and b/docs/en/Tutorials/images/blazor-delete-book-action-2.png differ diff --git a/docs/en/Tutorials/images/blazor-edit-book-action-3.png b/docs/en/Tutorials/images/blazor-edit-book-action-3.png new file mode 100644 index 0000000000..7052be4a33 Binary files /dev/null and b/docs/en/Tutorials/images/blazor-edit-book-action-3.png differ diff --git a/docs/en/Tutorials/images/blazor-edit-book-modal-2.png b/docs/en/Tutorials/images/blazor-edit-book-modal-2.png new file mode 100644 index 0000000000..8350ccb2ee Binary files /dev/null and b/docs/en/Tutorials/images/blazor-edit-book-modal-2.png differ diff --git a/docs/en/Tutorials/images/blazor-new-book-modal-2.png b/docs/en/Tutorials/images/blazor-new-book-modal-2.png new file mode 100644 index 0000000000..fb4877fb9d Binary files /dev/null and b/docs/en/Tutorials/images/blazor-new-book-modal-2.png differ diff --git a/docs/en/Tutorials/images/book-create-modal-with-author-2.png b/docs/en/Tutorials/images/book-create-modal-with-author-2.png new file mode 100644 index 0000000000..fdfef46b33 Binary files /dev/null and b/docs/en/Tutorials/images/book-create-modal-with-author-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-actions-buttons-2.png b/docs/en/Tutorials/images/bookstore-actions-buttons-2.png new file mode 100644 index 0000000000..a67dabe995 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-actions-buttons-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-added-author-to-book-list-2.png b/docs/en/Tutorials/images/bookstore-added-author-to-book-list-2.png new file mode 100644 index 0000000000..ca39f239c1 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-added-author-to-book-list-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-added-authors-to-modals-2.png b/docs/en/Tutorials/images/bookstore-added-authors-to-modals-2.png new file mode 100644 index 0000000000..f2541ea6de Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-added-authors-to-modals-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-angular-author-selection-2.png b/docs/en/Tutorials/images/bookstore-angular-author-selection-2.png new file mode 100644 index 0000000000..96b1688186 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-angular-author-selection-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-angular-authors-page-2.png b/docs/en/Tutorials/images/bookstore-angular-authors-page-2.png new file mode 100644 index 0000000000..7c6f9ce888 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-angular-authors-page-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-author-permissions-2.png b/docs/en/Tutorials/images/bookstore-author-permissions-2.png new file mode 100644 index 0000000000..26911fc664 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-author-permissions-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-author-permissions-3.png b/docs/en/Tutorials/images/bookstore-author-permissions-3.png new file mode 100644 index 0000000000..6b53bea4ed Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-author-permissions-3.png differ diff --git a/docs/en/Tutorials/images/bookstore-authors-page-2.png b/docs/en/Tutorials/images/bookstore-authors-page-2.png new file mode 100644 index 0000000000..2ec2d42fe2 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-authors-page-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-authors-page-3.png b/docs/en/Tutorials/images/bookstore-authors-page-3.png new file mode 100644 index 0000000000..3db547f7f9 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-authors-page-3.png differ diff --git a/docs/en/Tutorials/images/bookstore-book-list-4.png b/docs/en/Tutorials/images/bookstore-book-list-4.png new file mode 100644 index 0000000000..3d0897339d Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-book-list-4.png differ diff --git a/docs/en/Tutorials/images/bookstore-book-list-angular.png b/docs/en/Tutorials/images/bookstore-book-list-angular.png new file mode 100644 index 0000000000..d308e1ca95 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-book-list-angular.png differ diff --git a/docs/en/Tutorials/images/bookstore-books-with-authorname-angular-2.png b/docs/en/Tutorials/images/bookstore-books-with-authorname-angular-2.png new file mode 100644 index 0000000000..324568ad32 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-books-with-authorname-angular-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-confirmation-popup-2.png b/docs/en/Tutorials/images/bookstore-confirmation-popup-2.png new file mode 100644 index 0000000000..4d6ebc2f83 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-confirmation-popup-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-create-dialog-3.png b/docs/en/Tutorials/images/bookstore-create-dialog-3.png new file mode 100644 index 0000000000..d945ed6337 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-create-dialog-3.png differ diff --git a/docs/en/Tutorials/images/bookstore-edit-button-3.png b/docs/en/Tutorials/images/bookstore-edit-button-3.png new file mode 100644 index 0000000000..a464d543e7 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-edit-button-3.png differ diff --git a/docs/en/Tutorials/images/bookstore-edit-delete-actions-2.png b/docs/en/Tutorials/images/bookstore-edit-delete-actions-2.png new file mode 100644 index 0000000000..98b09ca712 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-edit-delete-actions-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-empty-new-book-modal-2.png b/docs/en/Tutorials/images/bookstore-empty-new-book-modal-2.png new file mode 100644 index 0000000000..d04819407d Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-empty-new-book-modal-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-final-actions-dropdown-2.png b/docs/en/Tutorials/images/bookstore-final-actions-dropdown-2.png new file mode 100644 index 0000000000..47a295e69e Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-final-actions-dropdown-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-new-author-modal-2.png b/docs/en/Tutorials/images/bookstore-new-author-modal-2.png new file mode 100644 index 0000000000..628dcd4db5 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-new-author-modal-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-new-book-button-3.png b/docs/en/Tutorials/images/bookstore-new-book-button-3.png new file mode 100644 index 0000000000..9ac6e0d45e Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-new-book-button-3.png differ diff --git a/docs/en/Tutorials/images/bookstore-new-book-button-small-2.png b/docs/en/Tutorials/images/bookstore-new-book-button-small-2.png new file mode 100644 index 0000000000..8540cbb18d Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-new-book-button-small-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-new-book-form-v3.png b/docs/en/Tutorials/images/bookstore-new-book-form-v3.png new file mode 100644 index 0000000000..6dca81546c Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-new-book-form-v3.png differ diff --git a/docs/en/Tutorials/images/bookstore-new-menu-item-2.png b/docs/en/Tutorials/images/bookstore-new-menu-item-2.png new file mode 100644 index 0000000000..3353eec83c Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-new-menu-item-2.png differ diff --git a/docs/en/Tutorials/images/bookstore-permissions-ui-2.png b/docs/en/Tutorials/images/bookstore-permissions-ui-2.png new file mode 100644 index 0000000000..d0110b0877 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-permissions-ui-2.png differ diff --git a/docs/en/UI/Angular/Account-Module.md b/docs/en/UI/Angular/Account-Module.md index d3934ab18b..28852f6b4d 100644 --- a/docs/en/UI/Angular/Account-Module.md +++ b/docs/en/UI/Angular/Account-Module.md @@ -61,16 +61,23 @@ npm install @volo/abp.ng.account Open the `app.module.ts` and add `AccountPublicConfigModule.forRoot()` to the imports array as shown below: +> Ensure that the `Account Layout Module` has been added if you are using the Lepton X theme. If you miss the step, you will get an error message that says `Account layout not found. Please check your configuration. If you are using LeptonX, please make sure you have added "AccountLayoutModule.forRoot()" to your app.module configuration.` when you try to access the account pages. Otherwise, you can skip adding the `AccountLayoutModule` step. + + ```js // app.module.ts import { AccountPublicConfigModule } from '@volo/abp.ng.account/public/config'; +// if you are using or want to use Lepton X, you should add AccountLayoutModule +// import { AccountLayoutModule } from '@volosoft/abp.ng.theme.lepton-x/account' + //... @NgModule({ imports: [ //... - AccountPublicConfigModule.forRoot() + AccountPublicConfigModule.forRoot(), + // AccountLayoutModule.forRoot() // Only for Lepton X ], //... }) diff --git a/docs/en/UI/Angular/OAuth-Module.md b/docs/en/UI/Angular/OAuth-Module.md new file mode 100644 index 0000000000..e7a62d1d2e --- /dev/null +++ b/docs/en/UI/Angular/OAuth-Module.md @@ -0,0 +1,21 @@ +# ABP OAuth Package +The authentication functionality has been moved from @abp/ng.core to @abp/ng.ouath since v7.0. + +If your app is version 7.0 or higher, you should include "AbpOAuthModule.forRoot()" in your app.module.ts as an import after "CoreModule.forRoot(...)". + +Those abstractions can be found in the @abp/ng-core packages. +- `AuthService` (the class that implements the IAuthService interface). +- `NAVIGATE_TO_MANAGE_PROFILE` Inject token. +- `AuthGuard` (the class that implements the IAuthGuard interface). +- `ApiInterceptor` (the class that implements the IApiInterceptor interface). + +Those base classes are overridden by the "AbpOAuthModule" for oAuth. There are also three functions provided with AbpOAuthModule. + +- `PIPE_TO_LOGIN_FN_KEY` a provide that calls a function when the user is not authenticated. The function should be PipeToLoginFn type. +- `SET_TOKEN_RESPONSE_TO_STORAGE_FN_KEY` a provide that calls a function when the user is authenticated. The function should be SetTokenResponseToStorageFn type. +- `CHECK_AUTHENTICATION_STATE_FN_KEY` a provide that calls a function when the user is authenticated and stores the auth state. The function should be CheckAuthenticationStateFn type. +The tokens and interfaces are in the `@abp/ng.core` package but the implementation of these interfaces is in the `@abp/ng.oauth` package. + +If you want to make your own authentication system, you must also change these 'abstract' classes. + +ApiInterceptor is provided by `@abp/ng.core` but overridden with `@abp/ng.oauth`. The ApiInterceptor adds the token, accepted-language, and tenant id to the header of the HTTP request. It also calls the http-wait service. diff --git a/docs/en/UI/Angular/Quick-Start.md b/docs/en/UI/Angular/Quick-Start.md index e3f25ceb01..768b3b7b67 100644 --- a/docs/en/UI/Angular/Quick-Start.md +++ b/docs/en/UI/Angular/Quick-Start.md @@ -37,19 +37,9 @@ This command will prepare a solution with an Angular and a .NET Core project in To continue reading without checking other methods, visit [Angular project structure section](#angular-project-structure). -### 2. Direct Download +### 2. Generating a CLI Command from Get Started Page -You may [download a solution scaffold directly on ABP.io](https://abp.io/get-started) if you are more comfortable with GUI or simply want to try ABP without installing the CLI. - -Please do the following: - -1. Click on the "DIRECT DOWNLOAD" tab. -2. Fill out the short form about your project. -3. Click on the "Create now" button. - -...and a customized download will start in a few seconds. - -To avoid IDE warnings, run `yarn` or `npm install` in the Angular root folder before you continue to the next section. It is not imperative but recommended. +You can generate a CLI command on the [get started page of the abp.io website](https://abp.io/get-started). Then, use the command on your terminal to create a new [Startup Template](../../Startup-Templates/Index.md). ## Angular Project Structure diff --git a/docs/en/UI/Angular/Theme-Configurations.md b/docs/en/UI/Angular/Theme-Configurations.md index 1ec4f56c5e..e663955667 100644 --- a/docs/en/UI/Angular/Theme-Configurations.md +++ b/docs/en/UI/Angular/Theme-Configurations.md @@ -43,6 +43,16 @@ Theme packages no longer import styles as CSS modules as of ABP version 6.0. The "inject": false, "bundleName": "abp-bundle.rtl" }, +{ + "input":"node_modules/@volo/ngx-lepton-x.lite/assets/css/font-bundle.rtl.css", + "inject":false, + "bundleName":"font-bundle.rtl" +} +,{ + "input":"node_modules/@volo/ngx-lepton-x.lite/assets/css/font-bundle.css", + "inject":false, + "bundleName":"font-bundle" +}, ``` ## Theme Basic @@ -150,7 +160,18 @@ Theme packages no longer import styles as CSS modules as of ABP version 6.0. The "input": "node_modules/@volosoft/abp.ng.theme.lepton-x/assets/css/abp-bundle.rtl.css", "inject": false, "bundleName": "abp-bundle.rtl" -} +}, +{ + "input": "node_modules/@volosoft/ngx-lepton-x/assets/css/font-bundle.css", + "inject": false, + "bundleName": "font-bundle" +}, +{ + "input": "node_modules/@volosoft/ngx-lepton-x/assets/css/font-bundle.rtl.css", + "inject": false, + "bundleName": "font-bundle.rtl" +}, + ``` ## Theme Lepton diff --git a/docs/en/UI/AspNetCore/AutoComplete-Select.md b/docs/en/UI/AspNetCore/AutoComplete-Select.md index 86d8c14b8a..e0ad7ceb24 100644 --- a/docs/en/UI/AspNetCore/AutoComplete-Select.md +++ b/docs/en/UI/AspNetCore/AutoComplete-Select.md @@ -22,7 +22,8 @@ A simple usage is presented below. data-autocomplete-display-property="name" data-autocomplete-value-property="id" data-autocomplete-items-property="items" - data-autocomplete-filter-param-name="filter"> + data-autocomplete-filter-param-name="filter" + data-autocomplete-allow-clear="true"> @SelectedAuthor.Name @@ -38,6 +39,8 @@ The select must have the `auto-complete-select` class and the following attribut - `data-autocomplete-filter-param-name`: * Filter text property name. _(For example: `filter`)_. - `data-autocomplete-selected-item-name`: Text to display as selected item. - `data-autocomplete-parent-selector`: jQuery selector expression for parent DOM. _(If it's in a modal, it's suggested to send the modal selector as this parameter)_. +- `data-autocomplete-allow-clear`: If `true`, it'll allow to clear the selected value. Default value: `false`. +- `data-autocomplete-placeholder`: Placeholder text to display when no value is selected. Also, selected value(s) should be defined with the `` tags inside select, since pagination is applied and the selected options might haven't loaded yet. diff --git a/docs/en/UI/AspNetCore/Forms-Validation.md b/docs/en/UI/AspNetCore/Forms-Validation.md index 48d7937013..257752a517 100644 --- a/docs/en/UI/AspNetCore/Forms-Validation.md +++ b/docs/en/UI/AspNetCore/Forms-Validation.md @@ -201,6 +201,27 @@ In this case, you can add an entry to the localization file using the key `MyNam > If you use the `[DisplayName]` but not add a corresponding entity to the localization file, then ABP Framework shows the given key as the field name, `MyNameKey` for this case. So, it provides a way to specify a hard coded display name even if you don't need to use the localization system. +### Enum Localization + +Enum members are also automatically localized wherever possible. For example, when we added `` to the form (like we did in the *ABP Form Tag Helpers* section), ABP can automatically fill the localized names of Enum members. To enabled it, you should define the localized values in your localization JSON file. Example entries for the `Genre` Enum defined in the *ABP Form Tag Helpers* section: + +````json +"Enum:Genre.0": "Classic movie", +"Enum:Genre.1": "Action movie", +"Enum:Genre.2": "Fiction", +"Enum:Genre.3": "Fantasy", +"Enum:Genre.4": "Animation/Cartoon" +```` + +You can use one of the following syntaxes for the localization keys: + +* `Enum:.