diff --git a/Directory.Packages.props b/Directory.Packages.props index 9b3c349f59..81225fdbde 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,8 +13,8 @@ - - + + @@ -51,64 +51,65 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - + - - - + + + + @@ -120,15 +121,15 @@ - - - - - + + + + + - + @@ -155,19 +156,19 @@ - + - + - - - + + + - + @@ -177,4 +178,4 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 31db04cba2..7cf41d47eb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ABP Framework ![build and test](https://img.shields.io/github/actions/workflow/status/abpframework/abp/build-and-test.yml?branch=dev&style=flat-square) ๐Ÿ”น [![codecov](https://codecov.io/gh/abpframework/abp/branch/dev/graph/badge.svg?token=jUKLCxa6HF)](https://codecov.io/gh/abpframework/abp) ๐Ÿ”น [![NuGet](https://img.shields.io/nuget/v/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) ๐Ÿ”น [![NuGet (with prereleases)](https://img.shields.io/nuget/vpre/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) ๐Ÿ”น [![MyGet (nightly builds)](https://img.shields.io/myget/abp-nightly/vpre/Volo.Abp.svg?style=flat-square)](https://abp.io/docs/latest/release-info/nightly-builds) ๐Ÿ”น -[![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) ๐Ÿ”น [![Code of Conduct](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](https://github.com/abpframework/abp/blob/dev/CODE_OF_CONDUCT.md) ๐Ÿ”น [![CLA Signed](https://cla-assistant.io/readme/badge/abpframework/abp)](https://cla-assistant.io/abpframework/abp) ๐Ÿ”น [![Discord Shield](https://discord.com/api/guilds/951497912645476422/widget.png?style=shield)](https://discord.gg/abp) +[![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) ๐Ÿ”น [![Code of Conduct](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](https://github.com/abpframework/abp/blob/dev/CODE_OF_CONDUCT.md) ๐Ÿ”น [![CLA Signed](https://cla-assistant.io/readme/badge/abpframework/abp)](https://cla-assistant.io/abpframework/abp) ๐Ÿ”น [![Discord Shield](https://discord.com/api/guilds/951497912645476422/widget.png?style=shield)](https://abp.io/join-discord) [ABP](https://abp.io/) offers an **opinionated architecture** to build enterprise software solutions with **best practices** on top of the **.NET** and the **ASP.NET Core** platforms. It provides the fundamental infrastructure, production-ready startup templates, pre-built application modules, UI themes, tooling, guides and documentation to implement that architecture properly and **automate the details** and repetitive works as much as possible. @@ -121,4 +121,4 @@ GitHub repository stars are an important indicator of popularity and the size of ## Discord Server -We have a Discord server where you can chat with other ABP users. Share your ideas, report technical issues, showcase your creations, share the tips that worked for you and catch up with the latest news and announcements about ABP Framework. Join ๐Ÿ‘‰ https://discord.gg/abp. +We have a Discord server where you can chat with other ABP users. Share your ideas, report technical issues, showcase your creations, share the tips that worked for you and catch up with the latest news and announcements about ABP Framework. Join ๐Ÿ‘‰ https://abp.io/join-discord. diff --git a/docs/en/Blog-Posts/2022-05-09 v5_3_Preview/POST.md b/docs/en/Blog-Posts/2022-05-09 v5_3_Preview/POST.md index b23dab0cd1..dfc5a7d67e 100644 --- a/docs/en/Blog-Posts/2022-05-09 v5_3_Preview/POST.md +++ b/docs/en/Blog-Posts/2022-05-09 v5_3_Preview/POST.md @@ -255,4 +255,4 @@ We've created an official ABP Discord server so the ABP Community can interact w Thanks to the ABP Community, **700+** people joined our Discord Server so far and it grows every day. -You can join our Discord Server from [here](https://discord.gg/abp), if you haven't yet. \ No newline at end of file +You can join our Discord Server from [here](https://abp.io/join-discord), if you haven't yet. \ No newline at end of file diff --git a/docs/en/Blog-Posts/2024-12-15-ABP-Studio-R2R/POST.md b/docs/en/Blog-Posts/2024-12-15-ABP-Studio-R2R/POST.md new file mode 100644 index 0000000000..e4c37eec07 --- /dev/null +++ b/docs/en/Blog-Posts/2024-12-15-ABP-Studio-R2R/POST.md @@ -0,0 +1,67 @@ +# ABP Studio Goes AOT: Faster Startups with Ready-to-Run (R2R) Publishing + +We're excited that [ABP Studio](https://abp.io/studio) now supports [Ready-to-Run (R2R) publishing](https://learn.microsoft.com/en-us/dotnet/core/deploying/ready-to-run) (starting from v0.9.16+), a hybrid form of ahead-of-time (AOT) compilation. This enhancement significantly improves the startup time and overall performance of ABP Studio, making it faster and more performant than ever before. + +Let's dive into what R2R publishing is, how it works, and the benefits it brings to ABP Studio. + +## What is Ready-to-Run (R2R) Publishing? + +Ready-to-Run (R2R) is a form of AOT compilation available in the .NET ecosystem. Unlike traditional just-in-time (JIT) compilation, R2R precompiles parts of your application to native code before deployment. This precompiled code helps reduce the startup time by minimizing the work needed during runtime. + +However, R2R isn't a complete AOT compilation. Instead, it's a hybrid approach because it stores both: + +* **Native code for precompiled methods** (to improve startup time and performance) + +* **Intermediate Language (IL) code** for methods that may need further JIT compilation + +This hybrid nature is why R2R binaries are typically larger. For ABP Studio, the storage size increased by ~150 MB with R2R enabled, but the trade-off is well worth it for the performance and startup-time gains. + +## How R2R (Ready-to-Run) Improves ABP Studio + +### Faster Startup Time ๐Ÿš€ + +One of the biggest advantages of R2R publishing is its impact on startup times. In our local tests, enabling R2R resulted in startup times being **reduced by 2.5x** โฌ‡๏ธ. + +This means you can get to work faster, without waiting for the application to being startup from the beginning. Whether you're launching ABP Studio to manage projects, generate code, or deploy applications, the improved responsiveness is noticeable. + +### Performance Enhancements ๐Ÿ“ˆ + +In addition to faster startups, R2R publishing contributes to overall performance improvements. By precompiling frequently used methods, R2R reduces the workload on the JIT compiler during execution, leading to smoother and more efficient operations. + +### Trade-offs: Increased Storage Size ๐Ÿ†™ + +With great performance comes a slight trade-off: storage size. R2R binaries include both **native** and **IL code**, which increases the file size. In the case of ABP Studio, the storage footprint increased by ~150 MB. However, the substantial improvements in speed and responsiveness make this a worthwhile investment. + +## How to Enable R2R Publishing in Your Applications? + +If you're developing applications and want to benefit from R2R, here's a quick guide on how to enable it in your .NET projects: + +1. You can add the following configuration to your final project's `.csproj` file: + +```xml + + true + +``` + +2. Then, publish your application with the `dotnet publish` command: + +```bash +dotnet publish -c Release +``` + +Alternatively, you can specify the _PublishReadyToRun_ flag directly to the `dotnet publish` command as follows: + +```bash +dotnet publish -c Release -r win-x64 -p:PublishReadyToRun=true +``` + +That's it! Your application will now include precompiled native code for faster startup and great performance benefits. + +> Please refer to the [official documentation](https://learn.microsoft.com/en-us/dotnet/core/deploying/ready-to-run) before publishing your application with R2R. + +## Conclusion + +As ABP team, we're always looking for ways to improve the developer experience. By adopting **Ready-to-Run (R2R) publishing** for ABP Studio, we're aiming to deliver a faster and more efficient tool for your development needs. + +Stay tuned for more updates and enhancements as we continue to optimize ABP Studio and please provide us with your invaluable feedback. \ No newline at end of file diff --git a/docs/en/Community-Articles/2022-09-15-Grpc-Demo/POST.md b/docs/en/Community-Articles/2022-09-15-Grpc-Demo/POST.md index b3870ff067..f639d60b0c 100644 --- a/docs/en/Community-Articles/2022-09-15-Grpc-Demo/POST.md +++ b/docs/en/Community-Articles/2022-09-15-Grpc-Demo/POST.md @@ -240,3 +240,7 @@ gRPC on .NET has different approaches, features, configurations and more details * You can find the completed source code here: https://github.com/abpframework/abp-samples/tree/master/GrpcDemo2 * You can also see all the changes I've done in this article here: https://github.com/abpframework/abp-samples/pull/200/files + +## See Also + +* [Consuming gRPC Services from Blazor WebAssembly Application Using gRPC-Web](https://abp.io/community/articles/consuming-grpc-services-from-blazor-webassembly-application-using-grpcweb-dqjry3rv) diff --git a/docs/en/Community-Articles/2024-01-18-ABP-Now-Supports-Keyed-Services/POST.md b/docs/en/Community-Articles/2024-01-18-ABP-Now-Supports-Keyed-Services/POST.md index a5f2c694ed..d100838b1a 100644 --- a/docs/en/Community-Articles/2024-01-18-ABP-Now-Supports-Keyed-Services/POST.md +++ b/docs/en/Community-Articles/2024-01-18-ABP-Now-Supports-Keyed-Services/POST.md @@ -187,11 +187,21 @@ On the other hand, resolving keyed services from `LazyServiceProvider` is not su ### Automatically Registering Keyed Services -Currently, if you want to register a keyed service, you need to do it manually as we see in the previous sections by using one of the overloads (`.AddKeyedTransient`, `.AddKeyedScoped` and `.AddKeyedSingleton`). +ABP provides the `ExposeKeyedServiceAttribute` to control which keyed services are provided by the related class. -It would be good if we could make this process automatically and not need to manually register services, and for that purpose, I have [created an issue](https://github.com/abpframework/abp/issues/18794) that aims to introduce an attribute, which allows us to automatically register multiple services as keyed services. +For example, if you want to register a keyed service as a transient dependency, you can do it as follows: -You can [follow the issue](https://github.com/abpframework/abp/issues/18794) if you are considering using keyed services in your application and don't want to register them manually. +```csharp +[ExposeKeyedService("taxCalculator")] +[ExposeKeyedService("calculator")] +public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency +{ +} +``` + +> Notice that the ExposeKeyedServiceAttribute only exposes the keyed services. So, you can not inject the ITaxCalculator or ICalculator interfaces in your application without using the FromKeyedServicesAttribute as shown in the example above. If you want to expose both keyed and non-keyed services, you can use the ExposeServicesAttribute and ExposeKeyedServiceAttribute attributes altogether. + +Please refer to the [Dependency Injection document](https://abp.io/docs/latest/framework/fundamentals/dependency-injection#exposekeyedservice-attribute) for further info. ## Summary diff --git a/docs/en/Community-Articles/2024-11-25-Global-Assets/POST.md b/docs/en/Community-Articles/2024-11-25-Global-Assets/POST.md new file mode 100644 index 0000000000..33dd9eb292 --- /dev/null +++ b/docs/en/Community-Articles/2024-11-25-Global-Assets/POST.md @@ -0,0 +1,309 @@ +# ABP Global Assets - New way to bundle JavaScript/CSS files in Blazor WebAssembly app + +We have introduced a new feature in the ABP framework to bundle the `JavaScript/CSS` files in the Blazor wasm app. This feature is called `Global Assets`. +With this feature, you don't need to run the `abp bundle` command to manually create/maintain the `global.js` and `global.css` files in your Blazor wasm app. + +## How Global Assets works? + +The new `Blazor wasm app` has two projects: + +1. `MyProjectName` (ASP.NET Core app) +2. `MyProjectName.Client` (Blazor wasm app) + +The `MyProjectName` reference the `MyProjectName.Client` project, and will be the entry point of the application, which means the `MyProjectName` project will be the `host` project of the `MyProjectName.Client` project. + +The static/virtual files of `MyProjectName` can be accessed by the `MyProjectName.Client` project, so we can create dynamic global assets in the `MyProjectName` project and use them in the `MyProjectName.Client` project. + +## How it works in ABP? + +We have created a new package `WebAssembly.Theme.Bundling` for the theme `WebAssembly` module and used the `Volo.Abp.AspNetCore.Mvc.UI.Bundling.BundleContributor` to add `JavaScript/CSS` files to the bundling system. + +* LeptonXLiteTheme: `AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule` +* LeptonXTheme: `AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule` +* LeptonTheme: `AbpAspNetCoreComponentsWebAssemblyLeptonThemeBundlingModule` +* BasicTheme: `AbpAspNetCoreComponentsWebAssemblyBasicThemeBundlingModule` + +The new `ThemeBundlingModule` only depends on `AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule(new package)`. It's an `abstractions module`, which only depends on `AbpAspNetCoreMvcUiBundlingAbstractionsModule`. + +We will get all `JavaScript/CSS` files on `OnApplicationInitializationAsync` method of `AbpAspNetCoreMvcUiBundlingModule` from bundling system and add them to `IDynamicFileProvider` service. After that, we can access the `JavaScript/CSS` files in the Blazor wasm app. + +## Add the Global Assets in the module + +If your module has `JavaScript/CSS` files that need to the bundling system, You have to create a new project(`YourModuleName.Blazor.WebAssembly.Bundling`) to your module solution, and reference the new project in the `MyProjectName` project and module dependencies. + +The new project should **only** depend on the `AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule` and define `BundleContributor` classes to contribute the `JavaScript/CSS` files. + +> Q: The new project(`YourModuleName.Blazor.WebAssembly.Bundling`) doesn't have the `libs/myscript.js` and `libs/myscript.css` files why the files can be added to the bundling system? + +> A: Because the `MyProjectName.Client` will depend on the `MyBlazorModule(YourModuleName.Blazor)` that contains the `JavaScript/CSS` files, The `MyProjectName` is referencing the `MyProjectName.Client` project, so the `MyProjectName` project can access the `JavaScript/CSS` files in the `MyProjectName.Client` project and add them to the bundling system. + +```csharp +[DependsOn( + typeof(AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule) +)] +public class MyBlazorWebAssemblyBundlingModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + // Script Bundles + options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global).AddContributors(typeof(MyModuleBundleScriptContributor)); + + // Style Bundles + options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global).AddContributors(typeof(MyModuleBundleStyleBundleContributor)); + }); + } +} +``` + +```csharp +public class MyModuleBundleScriptContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/MyModule.Blazor/libs/myscript.js"); + } +} + +public class MyModuleBundleStyleBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/MyModule.Blazor/libs/myscript.css"); + } +} +``` + +## Use the Global Assets in the Blazor WASM + +### MyCompanyName.MyProjectName.Blazor + +Convert your `MyCompanyName.MyProjectName.Blazor` project to integrate the `ABP module` system and depend on the `AbpAspNetCoreMvcUiBundlingModule` and `AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule`: + +* The `AbpAspNetCoreMvcUiBundlingModule` uses to create the `JavaScript/CSS` files to virtual files. +* The `AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule` uses to add theme `JavaScript/CSS` to the bundling system. + +Here is how your project files look like: + +**`Program.cs`:** + +```csharp +public class Program +{ + public async static Task Main(string[] args) + { + //... + + var builder = WebApplication.CreateBuilder(args); + builder.Host.AddAppSettingsSecretsJson() + .UseAutofac() + .UseSerilog(); + await builder.AddApplicationAsync(); + var app = builder.Build(); + await app.InitializeApplicationAsync(); + await app.RunAsync(); + return 0; + + //... + } +} +``` + +**`MyProjectNameBlazorModule.cs`:** + +```csharp +[DependsOn( + typeof(AbpAutofacModule), + typeof(AbpAspNetCoreMvcUiBundlingModule), + typeof(AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule) //Should be added! +)] +public class MyProjectNameBlazorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + //https://github.com/dotnet/aspnetcore/issues/52530 + Configure(options => + { + options.SuppressCheckForUnhandledSecurityMetadata = true; + }); + + // Add services to the container. + context.Services.AddRazorComponents() + .AddInteractiveWebAssemblyComponents(); + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var env = context.GetEnvironment(); + var app = context.GetApplicationBuilder(); + + // Configure the HTTP request pipeline. + if (env.IsDevelopment()) + { + app.UseWebAssemblyDebugging(); + } + else + { + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.MapAbpStaticAssets(); + app.UseRouting(); + app.UseAntiforgery(); + + app.UseConfiguredEndpoints(builder => + { + builder.MapRazorComponents() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(WebAppAdditionalAssembliesHelper.GetAssemblies()); + }); + } +} +``` + +**`MyCompanyName.MyProjectName.Blazor.csproj`:** + +```xml + + + + + + if you're using LeptonXTheme + + +``` + +### BlazorWebAssemblyBundlingModule in the ABP commercial + +Here is the list of `Bundling Modules` in the ABP commercial. If you're using the pro template, you should add them to the `MyCompanyName.MyProjectName.Blazor` project. + +| BundlingModules | Nuget Package | +|---------------------------------------------|-----------------------------------------------------| +| AbpAuditLoggingBlazorWebAssemblyBundlingModule | Volo.Abp.AuditLogging.Blazor.WebAssembly.Bundling | +| FileManagementBlazorWebAssemblyBundlingModule | Volo.FileManagement.Blazor.WebAssembly.Bundling | +| SaasHostBlazorWebAssemblyBundlingModule | Volo.Saas.Host.Blazor.WebAssembly.Bundling | +| ChatBlazorWebAssemblyBundlingModule | Volo.Chat.Blazor.WebAssembly.Bundling | +| CmsKitProAdminBlazorWebAssemblyBundlingModule | Volo.CmsKit.Pro.Admin.Blazor.WebAssembly.Bundling | + + +### MyCompanyName.MyProjectName.Blazor.Client + +1. Remove the `global.JavaScript/CSS` files from the `MyCompanyName.MyProjectName.Blazor`'s `wwwroot` folder. +2. Remove the `AbpCli:Bundle` section from the `appsettings.json` file. +3. Remove all BundleContributor classes that inherit from IBundleContributor. Then, create `MyProjectNameStyleBundleContributor` and `MyProjectNameScriptBundleContributor` classes to add your style and JavaScript files. Finally, add them to `AbpBundlingOptions`. + + +```cs +public class MyProjectNameStyleBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.Add(new BundleFile("main.css", true)); + } +} + + +public class MyProjectNameScriptBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.Add(new BundleFile("main.js", true)); + } +} +``` + +```cs +Configure(options => +{ + var globalStyles = options.StyleBundles.Get(BlazorWebAssemblyStandardBundles.Styles.Global); + globalStyles.AddContributors(typeof(MyProjectNameStyleBundleContributor)); + + var globalScripts = options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global); + globalScripts.AddContributors(typeof(MyProjectNameScriptBundleContributor)); +}); +``` + +## Use the Global Assets in the Blazor WebApp + +### MyCompanyName.MyProjectName.Blazor.WebApp + +Depending on the `AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule` in your `MyCompanyName.MyProjectName.Blazor.WebApp` project. + +* The `AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule` uses to add theme `JavaScript/CSS` to the bundling system. + + +### BlazorWebAssemblyBundlingModule in the ABP commercial + +Here is the list of `Bundling Modules` in the ABP commercial. If you're using the pro template, you should add them to the `MyCompanyName.MyProjectName.Blazor.WebApp` project. + +| BundlingModules | Nuget Package | +|---------------------------------------------|-----------------------------------------------------| +| AbpAuditLoggingBlazorWebAssemblyBundlingModule | Volo.Abp.AuditLogging.Blazor.WebAssembly.Bundling | +| FileManagementBlazorWebAssemblyBundlingModule | Volo.FileManagement.Blazor.WebAssembly.Bundling | +| SaasHostBlazorWebAssemblyBundlingModule | Volo.Saas.Host.Blazor.WebAssembly.Bundling | +| ChatBlazorWebAssemblyBundlingModule | Volo.Chat.Blazor.WebAssembly.Bundling | +| CmsKitProAdminBlazorWebAssemblyBundlingModule | Volo.CmsKit.Pro.Admin.Blazor.WebAssembly.Bundling | + + +### MyCompanyName.MyProjectName.Blazor.WebApp.Client + +1. Remove the `global.JavaScript/CSS` files from the `MyCompanyName.MyProjectName.Blazor.WebApp.Client`'s `wwwroot` folder. +2. Remove the `AbpCli:Bundle` section from the `appsettings.json` file. +3. Remove all BundleContributor classes that inherit from IBundleContributor. Then, create `MyProjectNameStyleBundleContributor` and `MyProjectNameScriptBundleContributor` classes to add your style and JavaScript files. Finally, add them to `AbpBundlingOptions`. + +```cs +public class MyProjectNameStyleBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.Add(new BundleFile("main.css", true)); + } +} + + +public class MyProjectNameScriptBundleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.Add(new BundleFile("main.js", true)); + } +} +``` + +```cs +Configure(options => +{ + var globalStyles = options.StyleBundles.Get(BlazorWebAssemblyStandardBundles.Styles.Global); + globalStyles.AddContributors(typeof(MyProjectNameStyleBundleContributor)); + + var globalScripts = options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global); + globalScripts.AddContributors(typeof(MyProjectNameScriptBundleContributor)); +}); +``` + +### Check the Global Assets + +Run the `MyProject` project and check the `https://localhost/global.js` and `https://localhost/global.css` files. You should be able to see the `JavaScript/CSS` files content from the Bundling system: + +![global](image.png) + +## GlobalAssets(AbpBundlingGlobalAssetsOptions) + +You can configure the JavaScript and CSS file names in the `GlobalAssets` property of the `AbpBundlingOptions` class. + +The default values are `global.js` and `global.css`. + +## Conclusion + +With the new `Global Assets` feature, you can easily bundle the `JavaScript/CSS` files in the Blazor wasm app. This feature is very useful for the Blazor wasm app, and it will save you a lot of time and effort. We hope you will enjoy this feature and use it in your projects. + +## References + +* [Virtual Files](https://docs.abp.io/en/abp/latest/Virtual-Files) +* [Bundle Contributors](https://abp.io/docs/latest/framework/ui/mvc-razor-pages/bundling-minification#bundle-contributors) +* [Global Assets Pull Request](https://github.com/abpframework/abp/pull/19968) + diff --git a/docs/en/Community-Articles/2024-11-25-Global-Assets/image.png b/docs/en/Community-Articles/2024-11-25-Global-Assets/image.png new file mode 100644 index 0000000000..8c5c52a4fc Binary files /dev/null and b/docs/en/Community-Articles/2024-11-25-Global-Assets/image.png differ diff --git a/docs/en/cli/index.md b/docs/en/cli/index.md index 61d5986588..645c12a715 100644 --- a/docs/en/cli/index.md +++ b/docs/en/cli/index.md @@ -112,7 +112,7 @@ abp cli clear-cache ### new -Generates a new solution based on the ABP [startup templates](../solution-templates). +Generates a new solution based on the ABP [startup templates](../solution-templates). See [new solution create sample commands](new-command-samples.md) Usage: diff --git a/docs/en/cli/new-command-samples.md b/docs/en/cli/new-command-samples.md index a8d469b10b..5aa65f4d18 100644 --- a/docs/en/cli/new-command-samples.md +++ b/docs/en/cli/new-command-samples.md @@ -21,7 +21,7 @@ The following commands are for creating Angular UI projects: * **Entity Framework Core**, **custom connection string**, creates the project in a new folder: ```bash - abp new Acme.BookStore -u angular -csf --connection-string Server=localhost;Database=MyDatabase;Trusted_Connection=True + abp new Acme.BookStore -u angular -csf --connection-string "Server=localhost;Database=MyDatabase;Trusted_Connection=True" ``` * **MongoDB**, default app template, mobile project included, creates solution in `C:\MyProjects\Acme.BookStore` diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index b1a335746b..d40fbb050c 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -5,7 +5,8 @@ "items": [ { "text": "Overview", - "path": "get-started" + "path": "get-started", + "isIndex": true }, { "text": "Single Layer Web Application", @@ -39,6 +40,10 @@ "path": "get-started/console.md" } ] + }, + { + "text": "Pre-Requirements", + "path": "get-started/pre-requirements.md" } ] }, @@ -47,14 +52,18 @@ "items": [ { "text": "Overview", - "path": "tutorials" + "path": "tutorials", + "isIndex": true }, { "text": "TODO Application", + "isLazyExpandable": true, + "path": "tutorials/todo", "items": [ { "text": "Overview", - "path": "tutorials/todo" + "path": "tutorials/todo", + "isIndex": true }, { "text": "Single-Layer Solution", @@ -68,10 +77,13 @@ }, { "text": "Book Store Application", + "isLazyExpandable": true, + "path": "tutorials/book-store", "items": [ { "text": "Overview", - "path": "tutorials/book-store" + "path": "tutorials/book-store", + "isIndex": true }, { "text": "1: Creating the Server Side", @@ -120,7 +132,8 @@ "items": [ { "text": "Overview", - "path": "tutorials/book-store-with-abp-suite" + "path": "tutorials/book-store-with-abp-suite", + "isIndex": true }, { "text": "1: Creating the Solution", @@ -146,10 +159,13 @@ }, { "text": "Modular Monolith Application", + "isLazyExpandable": true, + "path": "tutorials/modular-crm/index.md", "items": [ { "text": "Overview", - "path": "tutorials/modular-crm/index.md" + "path": "tutorials/modular-crm/index.md", + "isIndex": true }, { "text": "1: Creating the Initial Solution", @@ -190,7 +206,8 @@ "items": [ { "text": "Overview", - "path": "tutorials/microservice/index.md" + "path": "tutorials/microservice/index.md", + "isIndex": true }, { "text": "1: Creating the initial solution", @@ -222,6 +239,23 @@ } ] }, + { + "text": "Mobile Application Development", + "items": [ + { + "text": "Overview", + "path": "tutorials/mobile/index.md" + }, + { + "text": "MAUI", + "path": "tutorials/mobile/maui/index.md" + }, + { + "text": "React Native", + "path": "tutorials/mobile/react-native/index.md" + } + ] + }, { "text": "Community Articles", "path": "https://abp.io/community" @@ -233,14 +267,16 @@ "items": [ { "text": "Overview", - "path": "tools.md" + "path": "tools.md", + "isIndex": true }, { "text": "ABP CLI", "items": [ { "text": "Overview", - "path": "cli" + "path": "cli", + "isIndex": true }, { "text": "New Solution Sample Commands", @@ -253,7 +289,8 @@ "items": [ { "text": "Overview", - "path": "studio" + "path": "studio", + "isIndex": true }, { "text": "Installation", @@ -264,7 +301,8 @@ "items": [ { "text": "Overview", - "path": "studio/overview.md" + "path": "studio/overview.md", + "isIndex": true }, { "text": "Solution Explorer", @@ -307,7 +345,8 @@ "items": [ { "text": "Overview", - "path": "suite" + "path": "suite", + "isIndex": true }, { "text": "How to Install", @@ -330,7 +369,8 @@ "items": [ { "text": "Overview", - "path": "suite/generating-crud-page.md" + "path": "suite/generating-crud-page.md", + "isIndex": true }, { "text": "Creating Many-To-Many Relationship", @@ -382,7 +422,8 @@ "items": [ { "text": "Overview", - "path": "framework/fundamentals" + "path": "framework/fundamentals", + "isIndex": true }, { "text": "Application Startup", @@ -393,7 +434,8 @@ "items": [ { "text": "Overview", - "path": "framework/fundamentals/authorization.md" + "path": "framework/fundamentals/authorization.md", + "isIndex": true }, { "text": "Dynamic Claims", @@ -406,7 +448,8 @@ "items": [ { "text": "Overview", - "path": "framework/fundamentals/caching.md" + "path": "framework/fundamentals/caching.md", + "isIndex": true }, { "text": "Redis Cache", @@ -427,7 +470,8 @@ "items": [ { "text": "Overview", - "path": "framework/fundamentals/dependency-injection.md" + "path": "framework/fundamentals/dependency-injection.md", + "isIndex": true }, { "text": "AutoFac Integration", @@ -460,7 +504,8 @@ "items": [ { "text": "Overview", - "path": "framework/fundamentals/validation.md" + "path": "framework/fundamentals/validation.md", + "isIndex": true }, { "text": "FluentValidation Integration", @@ -475,7 +520,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure" + "path": "framework/infrastructure", + "isIndex": true }, { "text": "Audit Logging", @@ -486,7 +532,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure/background-jobs" + "path": "framework/infrastructure/background-jobs", + "isIndex": true }, { "text": "Hangfire Integration", @@ -507,7 +554,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure/background-workers" + "path": "framework/infrastructure/background-workers", + "isIndex": true }, { "text": "Quartz Integration", @@ -524,7 +572,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure/blob-storing" + "path": "framework/infrastructure/blob-storing", + "isIndex": true }, { "text": "Storage Providers", @@ -598,7 +647,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure/emailing.md" + "path": "framework/infrastructure/emailing.md", + "isIndex": true }, { "text": "MailKit Integration", @@ -615,7 +665,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure/event-bus" + "path": "framework/infrastructure/event-bus", + "isIndex": true }, { "text": "Local Event Bus", @@ -626,7 +677,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure/event-bus/distributed" + "path": "framework/infrastructure/event-bus/distributed", + "isIndex": true }, { "text": "Azure Service Bus Integration", @@ -693,7 +745,8 @@ "items": [ { "text": "Overview", - "path": "framework/infrastructure/text-templating" + "path": "framework/infrastructure/text-templating", + "isIndex": true }, { "text": "Razor Integration", @@ -720,14 +773,16 @@ "items": [ { "text": "Overview", - "path": "framework/architecture" + "path": "framework/architecture", + "isIndex": true }, { "text": "Module Development Best Practices", "items": [ { "text": "Overview", - "path": "framework/architecture/best-practices" + "path": "framework/architecture/best-practices", + "isIndex": true }, { "text": "Module Architecture", @@ -738,7 +793,8 @@ "items": [ { "text": "Overview", - "path": "framework/architecture/best-practices/domain-layer-overview.md" + "path": "framework/architecture/best-practices/domain-layer-overview.md", + "isIndex": true }, { "text": "Entities", @@ -759,7 +815,8 @@ "items": [ { "text": "Overview", - "path": "framework/architecture/best-practices/application-layer-overview.md" + "path": "framework/architecture/best-practices/application-layer-overview.md", + "isIndex": true }, { "text": "Application Services", @@ -776,7 +833,8 @@ "items": [ { "text": "Overview", - "path": "framework/architecture/best-practices/data-access-overview.md" + "path": "framework/architecture/best-practices/data-access-overview.md", + "isIndex": true }, { "text": "Entity Framework Core Integration", @@ -795,7 +853,8 @@ "items": [ { "text": "Overview", - "path": "framework/architecture/modularity/basics.md" + "path": "framework/architecture/modularity/basics.md", + "isIndex": true }, { "text": "Plug-In Modules", @@ -806,7 +865,8 @@ "items": [ { "text": "Overview", - "path": "framework/architecture/modularity/extending/customizing-application-modules-guide.md" + "path": "framework/architecture/modularity/extending/customizing-application-modules-guide.md", + "isIndex": true }, { "text": "Module Entity Extension System", @@ -829,14 +889,16 @@ "items": [ { "text": "Overview", - "path": "framework/architecture/domain-driven-design" + "path": "framework/architecture/domain-driven-design", + "isIndex": true }, { "text": "Domain Layer", "items": [ { "text": "Overview", - "path": "framework/architecture/domain-driven-design/domain-layer.md" + "path": "framework/architecture/domain-driven-design/domain-layer.md", + "isIndex": true }, { "text": "Entities & Aggregate Roots", @@ -865,7 +927,8 @@ "items": [ { "text": "Overview", - "path": "framework/architecture/domain-driven-design/application-layer.md" + "path": "framework/architecture/domain-driven-design/application-layer.md", + "isIndex": true }, { "text": "Application Services", @@ -894,6 +957,74 @@ { "text": "Microservices", "path": "framework/architecture/microservices" + }, + { + "text": "Module Development Best Practices", + "items": [ + { + "text": "Overview", + "path": "framework/architecture/best-practices" + }, + { + "text": "Module Architecture", + "path": "framework/architecture/best-practices/module-architecture.md" + }, + { + "text": "Domain Layer", + "items": [ + { + "text": "Overview", + "path": "framework/architecture/best-practices/domain-layer-overview.md" + }, + { + "text": "Entities", + "path": "framework/architecture/best-practices/entities.md" + }, + { + "text": "Repositories", + "path": "framework/architecture/best-practices/repositories.md" + }, + { + "text": "Domain Services", + "path": "framework/architecture/best-practices/domain-services.md" + } + ] + }, + { + "text": "Application Layer", + "items": [ + { + "text": "Overview", + "path": "framework/architecture/best-practices/application-layer-overview.md" + }, + { + "text": "Application Services", + "path": "framework/architecture/best-practices/application-services.md" + }, + { + "text": "Data Transfer Objects", + "path": "framework/architecture/best-practices/data-transfer-objects.md" + } + ] + }, + { + "text": "Data Access", + "items": [ + { + "text": "Overview", + "path": "framework/architecture/best-practices/data-access-overview.md" + }, + { + "text": "Entity Framework Core Integration", + "path": "framework/architecture/best-practices/entity-framework-core-integration.md" + }, + { + "text": "MongoDB Integration", + "path": "framework/architecture/best-practices/mongodb-integration.md" + } + ] + } + ] } ] }, @@ -902,14 +1033,16 @@ "items": [ { "text": "Overview", - "path": "framework/api-development" + "path": "framework/api-development", + "isIndex": true }, { "text": "ABP Endpoints", "items": [ { "text": "Overview", - "path": "framework/api-development/standard-apis" + "path": "framework/api-development/standard-apis", + "isIndex": true }, { "text": "Application Configuration", @@ -952,14 +1085,16 @@ "items": [ { "text": "Overview", - "path": "framework/ui" + "path": "framework/ui", + "isIndex": true }, { "text": "MVC / Razor Pages", "items": [ { "text": "Overview", - "path": "framework/ui/mvc-razor-pages/overall.md" + "path": "framework/ui/mvc-razor-pages/overall.md", + "isIndex": true }, { "text": "Navigation / Menus", @@ -1006,7 +1141,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/mvc-razor-pages/tag-helpers" + "path": "framework/ui/mvc-razor-pages/tag-helpers", + "isIndex": true }, { "text": "Form Elements", @@ -1047,7 +1183,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/mvc-razor-pages/theming.md" + "path": "framework/ui/mvc-razor-pages/theming.md", + "isIndex": true }, { "text": "The Basic Theme", @@ -1064,7 +1201,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/mvc-razor-pages/javascript-api" + "path": "framework/ui/mvc-razor-pages/javascript-api", + "isIndex": true }, { "text": "Localization", @@ -1125,7 +1263,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/mvc-razor-pages/customization-user-interface.md" + "path": "framework/ui/mvc-razor-pages/customization-user-interface.md", + "isIndex": true }, { "text": "Entity Action Extensions", @@ -1157,7 +1296,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/blazor/overall.md" + "path": "framework/ui/blazor/overall.md", + "isIndex": true }, { "text": "Navigation / Menu", @@ -1176,7 +1316,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/blazor/theming.md" + "path": "framework/ui/blazor/theming.md", + "isIndex": true }, { "text": "The Basic Theme", @@ -1298,7 +1439,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/angular/overview.md" + "path": "framework/ui/angular/overview.md", + "isIndex": true }, { "text": "Quick Start", @@ -1492,7 +1634,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/angular/theming.md" + "path": "framework/ui/angular/theming.md", + "isIndex": true }, { "text": "Configuration", @@ -1529,7 +1672,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/angular/extensions-overall.md" + "path": "framework/ui/angular/extensions-overall.md", + "isIndex": true }, { "text": "Entity Action Extensions", @@ -1591,7 +1735,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/react-native" + "path": "framework/ui/react-native", + "isIndex": true } ] }, @@ -1600,7 +1745,8 @@ "items": [ { "text": "Overview", - "path": "framework/ui/maui" + "path": "framework/ui/maui", + "isIndex": true } ] }, @@ -1629,14 +1775,16 @@ "items": [ { "text": "Overview", - "path": "framework/data" + "path": "framework/data", + "isIndex": true }, { "text": "Entity Framework Core", "items": [ { "text": "Overview", - "path": "framework/data/entity-framework-core" + "path": "framework/data/entity-framework-core", + "isIndex": true }, { "text": "Database Migrations", @@ -1706,7 +1854,8 @@ "items": [ { "text": "Overview", - "path": "solution-templates" + "path": "solution-templates", + "isIndex": true }, { "text": "Template Guide", @@ -1767,7 +1916,170 @@ }, { "text": "Microservice Solution", - "path": "solution-templates/microservice" + "isLazyExpandable": true, + "path": "solution-templates/microservice", + "items":[ + { + "text": "Overview", + "path": "solution-templates/microservice" + }, + { + "text": "Solution Structure", + "path": "solution-templates/microservice/solution-structure.md" + }, + { + "text": "Main Components", + "items": [ + { + "text": "Overview", + "path": "solution-templates/microservice/main-components" + }, + { + "text": "Microservices", + "path": "solution-templates/microservice/microservices.md" + }, + { + "text": "API Gateways", + "path": "solution-templates/microservice/api-gateways.md" + }, + { + "text": "Web Applications", + "path": "solution-templates/microservice/web-applications.md" + }, + { + "text": "Mobile Applications", + "path": "solution-templates/microservice/mobile-applications.md" + } + ] + }, + { + "text": "Built-In Features", + "items": [ + { + "text": "Overview", + "path": "solution-templates/microservice/built-in-features.md" + }, + { + "text": "Authentication", + "path": "solution-templates/microservice/authentication.md" + }, + { + "text": "Database configurations", + "path": "solution-templates/microservice/database-configurations.md" + }, + { + "text": "Logging (with Serilog and Elasticsearch)", + "path": "solution-templates/microservice/logging.md" + }, + { + "text": "Monitoring (with Prometheus and Grafana)", + "path": "solution-templates/microservice/monitoring.md" + }, + { + "text": "Swagger integration", + "path": "solution-templates/microservice/swagger.md" + }, + { + "text": "Permission management", + "path": "solution-templates/microservice/permission-management.md" + }, + { + "text": "Feature management", + "path": "solution-templates/microservice/feature-management.md" + }, + { + "text": "Localization system", + "path": "solution-templates/microservice/localization-system.md" + }, + { + "text": "Background Jobs", + "path": "solution-templates/microservice/background-jobs.md" + }, + { + "text": "Background Workers", + "path": "solution-templates/microservice/background-workers.md" + }, + { + "text": "Distributed Locking", + "path": "solution-templates/microservice/distributed-locking.md" + }, + { + "text": "Distributed Cache", + "path": "solution-templates/microservice/distributed-cache.md" + }, + { + "text": "Multi-Tenancy", + "path": "solution-templates/microservice/multi-tenancy.md" + }, + { + "text": "BLOB Storing", + "path": "solution-templates/microservice/blob-storing.md" + }, + { + "text": "CORS configuration", + "path": "solution-templates/microservice/cors-configuration.md" + } + ] + }, + { + "text": "Communication", + "items":[ + { + "text": "Overview", + "path": "solution-templates/microservice/communication.md" + }, + { + "text": "HTTP API Calls", + "path": "solution-templates/microservice/http-api-calls.md" + }, + { + "text": "gRPC Calls", + "path": "solution-templates/microservice/grpc-calls.md" + }, + { + "text": "Distributed Events", + "path": "solution-templates/microservice/distributed-events.md" + } + ] + }, + { + "text": "Helm Charts and Kubernetes", + "path": "solution-templates/microservice/helm-charts-and-kubernetes.md" + }, + { + "text": "Guides", + "items": [ + { + "text": "Overview", + "path": "solution-templates/microservice/guides.md" + }, + { + "text": "Adding new microservices", + "path": "solution-templates/microservice/adding-new-microservices.md" + }, + { + "text": "Adding new applications", + "path": "solution-templates/microservice/adding-new-applications.md" + }, + { + "text": "Adding new API gateways", + "path": "solution-templates/microservice/adding-new-api-gateways.md" + }, + { + "text": "Mono-repo vs multiple repository approaches", + "path": "solution-templates/microservice/mono-repo-vs-multiple-repository-approaches.md" + }, + { + "text": "Authoring unit and integration tests", + "path": "solution-templates/microservice/authoring-unit-and-integration-tests.md" + }, + { + "text": "How to use with ABP Suite", + "path": "solution-templates/microservice/how-to-use-with-abp-suite.md" + } + ] + } + ] }, { "text": "Application Module", @@ -1780,7 +2092,8 @@ "items": [ { "text": "Overview", - "path": "modules" + "path": "modules", + "isIndex": true }, { "text": "Account", @@ -1791,7 +2104,8 @@ "items": [ { "text": "Overview", - "path": "modules/account-pro.md" + "path": "modules/account-pro.md", + "isIndex": true }, { "text": "Tenant impersonation & User impersonation", @@ -1853,10 +2167,13 @@ }, { "text": "IdentityServer", + "isLazyExpandable": true, + "path": "modules/identity-server.md", "items": [ { "text": "Overview", - "path": "modules/identity-server.md" + "path": "modules/identity-server.md", + "isIndex": true }, { "text": "IdentityServer Migration Guide", @@ -1874,10 +2191,13 @@ }, { "text": "OpenIddict", + "isLazyExpandable": true, + "path": "modules/openiddict.md", "items": [ { "text": "Overview", - "path": "modules/openiddict.md" + "path": "modules/openiddict.md", + "isIndex": true }, { "text": "OpenIddict Migration Guide", @@ -1937,11 +2257,12 @@ "items": [ { "text": "Overview", - "path": "ui-themes" + "path": "ui-themes", + "isIndex": true }, { "text": "The Basic Theme", - "path": "framework/ui/mvc-razor-pages/basic-theme.md" + "path": "ui-themes/basic-theme" }, { "text": "LeptonX Theme", @@ -1954,7 +2275,8 @@ "items": [ { "text": "Overview", - "path": "testing/overall.md" + "path": "testing/overall.md", + "isIndex": true }, { "text": "Unit tests", @@ -1975,7 +2297,8 @@ "items": [ { "text": "Overview", - "path": "deployment" + "path": "deployment", + "isIndex": true }, { "text": "Configuring SSL certificate(HTTPS)", @@ -2008,7 +2331,8 @@ "items": [ { "text": "Overview", - "path": "samples" + "path": "samples", + "isIndex": true }, { "text": "EventHub", @@ -2033,7 +2357,8 @@ "items": [ { "text": "Overview", - "path": "https://abp.io/books" + "path": "https://abp.io/books", + "isIndex": true }, { "text": "Mastering ABP Framework", @@ -2050,7 +2375,8 @@ "items": [ { "text": "Overview", - "path": "release-info" + "path": "release-info", + "isIndex": true }, { "text": "Release Notes", diff --git a/docs/en/docs-params.json b/docs/en/docs-params.json index e108f1b554..4aa720e441 100644 --- a/docs/en/docs-params.json +++ b/docs/en/docs-params.json @@ -8,6 +8,7 @@ "Blazor": "Blazor WebAssembly", "BlazorServer": "Blazor Server", "BlazorWebApp": "Blazor WebApp", + "MAUIBlazor": "MAUI Blazor (Hybrid)", "NG": "Angular" } }, diff --git a/docs/en/framework/architecture/best-practices/application-services.md b/docs/en/framework/architecture/best-practices/application-services.md index d3ad44ca54..efe937e956 100644 --- a/docs/en/framework/architecture/best-practices/application-services.md +++ b/docs/en/framework/architecture/best-practices/application-services.md @@ -1,8 +1,14 @@ # Application Services Best Practices & Conventions +> This document offers best practices for implementing Application Services classes in your modules and applications based on Domain-Driven-Design principles. +> +> **Ensure you've read the [*Application Services*](../domain-driven-design/application-services.md) document first.** + +## General + * **Do** create an application service for each **aggregate root**. -### Application Service Interface +## Application Service Interface * **Do** define an `interface` for each application service in the **application contracts** package. * **Do** inherit from the `IApplicationService` interface. @@ -11,11 +17,11 @@ * **Do not** get/return entities for the service methods. * **Do** define DTOs based on the [DTO best practices](data-transfer-objects.md). -#### Outputs +### Outputs * **Avoid** to define too many output DTOs for same or related entities. Instead, define a **basic** and a **detailed** DTO for an entity. -##### Basic DTO +#### Basic DTO **Do** define a **basic** DTO for an aggregate root. @@ -44,7 +50,7 @@ public class IssueLabelDto } ``` -##### Detailed DTO +#### Detailed DTO **Do** define a **detailed** DTO for an entity if it has reference(s) to other aggregate roots. @@ -81,20 +87,20 @@ public class LabelDto : ExtensibleEntityDto } ```` -#### Inputs +### Inputs * **Do not** define any property in an input DTO that is not used in the service class. * **Do not** share input DTOs between application service methods. * **Do not** inherit an input DTO class from another one. * **May** inherit from an abstract base DTO class and share some properties between different DTOs in that way. However, should be very careful in that case because manipulating the base DTO would effect all related DTOs and service methods. Avoid from that as a good practice. -#### Methods +### Methods * **Do** define service methods as asynchronous with **Async** postfix. * **Do not** repeat the entity name in the method names. * Example: Define `GetAsync(...)` instead of `GetProductAsync(...)` in the `IProductAppService`. -##### Getting A Single Entity +#### Getting A Single Entity * **Do** use the `GetAsync` **method name**. * **Do** get Id with a **primitive** method parameter. @@ -104,7 +110,7 @@ public class LabelDto : ExtensibleEntityDto Task GetAsync(Guid id); ```` -##### Getting A List Of Entities +#### Getting A List Of Entities * **Do** use the `GetListAsync` **method name**. * **Do** get a single DTO argument for **filtering**, **sorting** and **paging** if necessary. @@ -117,7 +123,7 @@ Task GetAsync(Guid id); Task> GetListAsync(QuestionListQueryDto queryDto); ```` -##### Creating A New Entity +#### Creating A New Entity * **Do** use the `CreateAsync` **method name**. * **Do** get a **specialized input** DTO to create the entity. @@ -151,7 +157,7 @@ public class CreateQuestionDto : ExtensibleObject } ```` -##### Updating An Existing Entity +#### Updating An Existing Entity - **Do** use the `UpdateAsync` **method name**. - **Do** get a **specialized input** DTO to update the entity. @@ -167,7 +173,7 @@ Example: Task UpdateAsync(Guid id, UpdateQuestionDto updateQuestionDto); ```` -##### Deleting An Existing Entity +#### Deleting An Existing Entity - **Do** use the `DeleteAsync` **method name**. - **Do** get Id with a **primitive** method parameter. Example: @@ -176,7 +182,7 @@ Task UpdateAsync(Guid id, UpdateQuestionDto updateQuesti Task DeleteAsync(Guid id); ```` -##### Other Methods +#### Other Methods * **Can** define additional methods to perform operations on the entity. Example: @@ -186,7 +192,7 @@ Task VoteAsync(Guid id, VoteType type); This method votes a question and returns the current score of the question. -### Application Service Implementation +## Application Service Implementation * **Do** develop the application layer **completely independent from the web layer**. * **Do** implement application service interfaces in the **application layer**. @@ -195,30 +201,30 @@ This method votes a question and returns the current score of the question. * **Do** make all public methods **virtual**, so developers may inherit and override them. * **Do not** make **private** methods. Instead make them **protected virtual**, so developers may inherit and override them. -#### Using Repositories +### Using Repositories * **Do** use the specifically designed repositories (like `IProductRepository`). * **Do not** use generic repositories (like `IRepository`). -#### Querying Data +### Querying Data * **Do not** use LINQ/SQL for querying data from database inside the application service methods. It's repository's responsibility to perform LINQ/SQL queries from the data source. -#### Extra Properties +### Extra Properties * **Do** use either `MapExtraPropertiesTo` extension method ([see](../../fundamentals/object-extensions.md)) or configure the object mapper (`MapExtraProperties`) to allow application developers to be able to extend the objects and services. -#### Manipulating / Deleting Entities +### Manipulating / Deleting Entities * **Do** always get all the related entities from repositories to perform the operations on them. * **Do** call repository's Update/UpdateAsync method after updating an entity. Because, not all database APIs support change tracking & auto update. -#### Handle files +### Handle files * **Do not** use any web components like `IFormFile` or `Stream` in the application services. If you want to serve a file you can use `byte[]`. * **Do** use a `Controller` to handle file uploading then pass the `byte[]` of the file to the application service method. -#### Using Other Application Services +### Using Other Application Services * **Do not** use other application services of the same module/application. Instead; * Use domain layer to perform the required task. diff --git a/docs/en/framework/architecture/best-practices/data-transfer-objects.md b/docs/en/framework/architecture/best-practices/data-transfer-objects.md index e4caa6ed66..96194b4adb 100644 --- a/docs/en/framework/architecture/best-practices/data-transfer-objects.md +++ b/docs/en/framework/architecture/best-practices/data-transfer-objects.md @@ -1,5 +1,11 @@ # Data Transfer Objects Best Practices & Conventions +> This document offers best practices for implementing Data Transfer Object classes in your modules and applications based on Domain-Driven-Design principles. +> +> **Ensure you've read the [*Data Transfer Objects*](../domain-driven-design/data-transfer-objects.md) document first.** + +## General + * **Do** define DTOs in the **application contracts** package. * **Do** inherit from the pre-built **base DTO classes** where possible and necessary (like `EntityDto`, `CreationAuditedEntityDto`, `AuditedEntityDto`, `FullAuditedEntityDto` and so on). * **Do** inherit from the **extensible DTO** classes for the **aggregate roots** (like `ExtensibleAuditedEntityDto`), because aggregate roots are extensible objects and extra properties are mapped to DTOs in this way. diff --git a/docs/en/framework/architecture/best-practices/domain-services.md b/docs/en/framework/architecture/best-practices/domain-services.md index 565a67a715..55c731d207 100644 --- a/docs/en/framework/architecture/best-practices/domain-services.md +++ b/docs/en/framework/architecture/best-practices/domain-services.md @@ -1,6 +1,10 @@ # Domain Services Best Practices & Conventions -### Domain Service +> This document offers best practices for implementing Domain Service classes in your modules and applications based on Domain-Driven-Design principles. +> +> **Ensure you've read the [*Domain Services*](../domain-driven-design/domain-services.md) document first.** + +## Domain Services - **Do** define domain services in the **domain layer**. - **Do not** create interfaces for the domain services **unless** you have a good reason to (like mock and test different implementations). @@ -14,7 +18,7 @@ public class IssueManager : DomainService } ``` -### Domain Service Methods +## Domain Service Methods - **Do not** define `GET` methods. `GET` methods do not change the state of an entity. Hence, use the repository directly in the Application Service instead of Domain Service method. @@ -57,8 +61,6 @@ public async Task AssignToAsync(Issue issue, IdentityUser user) - **Do not** return `DTO`. Return only domain objects when you need. - **Do not** involve authenticated user logic. Instead, define extra parameter and send the related data of ` CurrentUser` from the Application Service layer. - - ## See Also * [Video tutorial](https://abp.io/video-courses/essentials/domain-services) diff --git a/docs/en/framework/architecture/best-practices/entities.md b/docs/en/framework/architecture/best-practices/entities.md index 319f1648ff..cc4b0e2a21 100644 --- a/docs/en/framework/architecture/best-practices/entities.md +++ b/docs/en/framework/architecture/best-practices/entities.md @@ -1,12 +1,16 @@ # Entity Best Practices & Conventions -### Entities +> This document offers best practices for implementing Aggregate Root and Entity classes in your modules and applications based on Domain-Driven-Design principles. +> +> **Ensure you've read the [*Entities*](../domain-driven-design/entities.md) document first.** + +## Entities Every aggregate root is also an entity. So, these rules are valid for aggregate roots too unless aggregate root rules override them. - **Do** define entities in the **domain layer**. -#### Primary Constructor +### Primary Constructor * **Do** define a **primary constructor** that ensures the validity of the entity on creation. Primary constructors are used to create a new instance of the entity by the application code. @@ -14,15 +18,15 @@ Every aggregate root is also an entity. So, these rules are valid for aggregate - **Do** always initialize sub collections in the primary constructor. - **Do not** generate `Guid` keys inside the constructor. Get it as a parameter, so the calling code will use `IGuidGenerator` to generate a new `Guid` value. -#### Parameterless Constructor +### Parameterless Constructor - **Do** always define a `protected` parameterless constructor to be compatible with ORMs. -#### References +### References - **Do** always **reference** to other aggregate roots **by Id**. Never add navigation properties to other aggregate roots. -#### Other Class Members +### Other Class Members - **Do** always define properties and methods as `virtual` (except `private` methods, obviously). Because some ORMs and dynamic proxy tools require it. - **Do** keep the entity as always **valid** and **consistent** within its own boundary. @@ -30,27 +34,27 @@ Every aggregate root is also an entity. So, these rules are valid for aggregate - **Do** define `public `, `internal` or `protected internal` (virtual) **methods** to change the properties (with non-public setters) if necessary. - **Do** return the entity object (`this`) from the setter methods. -### Aggregate Roots +## Aggregate Roots -#### Primary Keys +### Primary Keys * **Do** always use a **Id** property for the aggregate root key. * **Do not** use **composite keys** for aggregate roots. * **Do** use **Guid** as the **primary key** of all aggregate roots. -#### Base Class +### Base Class * **Do** inherit from the `AggregateRoot` or one of the audited classes (`CreationAuditedAggregateRoot`, `AuditedAggregateRoot` or `FullAuditedAggregateRoot`) based on requirements. -#### Aggregate Boundary +### Aggregate Boundary * **Do** keep aggregates **as small as possible**. Most of the aggregates will only have primitive properties and will not have sub collections. Consider these as design decisions: * **Performance** & **memory** cost of loading & saving aggregates (keep in mind that an aggregate is normally loaded & saved as a single unit). Larger aggregates will consume more CPU & memory. * **Consistency** & **validity** boundary. -### Example +## Example -#### Aggregate Root +### Aggregate Root ````C# public class Issue : FullAuditedAggregateRoot //Using Guid as the key/identifier @@ -130,7 +134,7 @@ public class Issue : FullAuditedAggregateRoot //Using Guid as the key/iden } ```` -#### The Entity +### Entity ````C# public class IssueLabel : Entity @@ -151,11 +155,12 @@ public class IssueLabel : Entity } ```` -### References +## References * Effective Aggregate Design by Vaughn Vernon http://dddcommunity.org/library/vernon_2011 - ## See Also + +## See Also * [Video tutorial](https://abp.io/video-courses/essentials/entities) \ No newline at end of file diff --git a/docs/en/framework/architecture/best-practices/entity-framework-core-integration.md b/docs/en/framework/architecture/best-practices/entity-framework-core-integration.md index 9a2081e4e5..960e0e69e4 100644 --- a/docs/en/framework/architecture/best-practices/entity-framework-core-integration.md +++ b/docs/en/framework/architecture/best-practices/entity-framework-core-integration.md @@ -1,12 +1,16 @@ # Entity Framework Core Integration Best Practices -> See [Entity Framework Core Integration document](../../data/entity-framework-core) for the basics of the EF Core integration. +> This document offers best practices for implementing Entity Framework Core integration in your modules and applications. +> +> **Ensure you've read the [*Entity Framework Core Integration*](../../data/entity-framework-core/index.md) document first.** + +## General - **Do** define a separated `DbContext` interface and class for each module. - **Do not** rely on lazy loading on the application development. - **Do not** enable lazy loading for the `DbContext`. -### DbContext Interface +## DbContext Interface - **Do** define an **interface** for the `DbContext` that inherits from `IEfCoreDbContext`. - **Do** add a `ConnectionStringName` **attribute** to the `DbContext` interface. @@ -23,7 +27,7 @@ public interface IIdentityDbContext : IEfCoreDbContext * **Do not** define `set;` for the properties in this interface. -### DbContext class +## DbContext class * **Do** inherit the `DbContext` from the `AbpDbContext` class. * **Do** add a `ConnectionStringName` attribute to the `DbContext` class. @@ -46,7 +50,7 @@ public class IdentityDbContext : AbpDbContext, IIdentityDbCon } ```` -### Table Prefix and Schema +## Table Prefix and Schema - **Do** add static `TablePrefix` and `Schema` **properties** to the `DbContext` class. Set default value from a constant. Example: @@ -58,7 +62,7 @@ public static string Schema { get; set; } = AbpIdentityConsts.DefaultDbSchema; - **Do** always use a short `TablePrefix` value for a module to create **unique table names** in a shared database. `Abp` table prefix is reserved for ABP core modules. - **Do** set `Schema` to `null` as default. -### Model Mapping +## Model Mapping - **Do** explicitly **configure all entities** by overriding the `OnModelCreating` method of the `DbContext`. Example: @@ -100,7 +104,7 @@ public static class IdentityDbContextModelBuilderExtensions * **Do** call `b.ConfigureByConvention();` for each entity mapping (as shown above). -### Repository Implementation +## Repository Implementation - **Do** **inherit** the repository from the `EfCoreRepository` class and implement the corresponding repository interface. Example: @@ -168,7 +172,7 @@ public override async Task> WithDetailsAsync() } ```` -### Module Class +## Module Class - **Do** define a module class for the Entity Framework Core integration package. - **Do** add `DbContext` to the `IServiceCollection` using the `AddAbpDbContext` method. diff --git a/docs/en/framework/architecture/best-practices/module-architecture.md b/docs/en/framework/architecture/best-practices/module-architecture.md index d050094656..c168b77f87 100644 --- a/docs/en/framework/architecture/best-practices/module-architecture.md +++ b/docs/en/framework/architecture/best-practices/module-architecture.md @@ -1,13 +1,13 @@ # Module Architecture Best Practices & Conventions -### Solution Structure +## Solution Structure * **Do** create a separated Visual Studio solution for every module. * **Do** name the solution as *CompanyName.ModuleName* (for core ABP modules, it's *Volo.Abp.ModuleName*). * **Do** develop the module as layered, so it has several packages (projects) those are related to each other. * Every package has its own module definition file and explicitly declares the dependencies for the depended packages/modules. -### Layers & Packages +## Layers & Packages The following diagram shows the packages of a well-layered module and dependencies of those packages between them: diff --git a/docs/en/framework/architecture/best-practices/mongodb-integration.md b/docs/en/framework/architecture/best-practices/mongodb-integration.md index 36b037c967..1930984e2e 100644 --- a/docs/en/framework/architecture/best-practices/mongodb-integration.md +++ b/docs/en/framework/architecture/best-practices/mongodb-integration.md @@ -1,8 +1,14 @@ # MongoDB Integration +> This document offers best practices for implementing MongoDB integration in your modules and applications. +> +> **Ensure you've read the [*MongoDB Integration*](../../data/entity-framework-core/index.md) document first.** + +## General + * Do define a separated `MongoDbContext` interface and class for each module. -### MongoDbContext Interface +## MongoDbContext Interface - **Do** define an **interface** for the `MongoDbContext` that inherits from `IAbpMongoDbContext`. - **Do** add a `ConnectionStringName` **attribute** to the `MongoDbContext` interface. @@ -17,7 +23,7 @@ public interface IAbpIdentityMongoDbContext : IAbpMongoDbContext } ```` -### MongoDbContext class +## MongoDbContext class - **Do** inherit the `MongoDbContext` from the `AbpMongoDbContext` class. - **Do** add a `ConnectionStringName` attribute to the `MongoDbContext` class. @@ -34,7 +40,7 @@ public class AbpIdentityMongoDbContext : AbpMongoDbContext, IAbpIdentityMongoDbC } ``` -### Collection Prefix +## Collection Prefix - **Do** add static `CollectionPrefix` **property** to the `DbContext` class. Set default value from a constant. Example: @@ -46,7 +52,7 @@ Used the same constant defined for the EF Core integration table prefix in this - **Do** always use a short `CollectionPrefix` value for a module to create **unique collection names** in a shared database. `Abp` collection prefix is reserved for ABP core modules. -### Collection Mapping +## Collection Mapping - **Do** explicitly **configure all aggregate roots** by overriding the `CreateModel` method of the `MongoDbContext`. Example: @@ -83,7 +89,7 @@ public static class AbpIdentityMongoDbContextExtensions } ``` -### Repository Implementation +## Repository Implementation - **Do** **inherit** the repository from the `MongoDbRepository` class and implement the corresponding repository interface. Example: @@ -124,7 +130,7 @@ public async Task FindByNormalizedUserNameAsync( * Using `IQueryable` makes the code as much as similar to the EF Core repository implementation and easy to write and read. * **Do** implement data filtering if it is not possible to use the `GetMongoQueryable()` method. -### Module Class +## Module Class - **Do** define a module class for the MongoDB integration package. - **Do** add `MongoDbContext` to the `IServiceCollection` using the `AddMongoDbContext` method. diff --git a/docs/en/framework/architecture/best-practices/repositories.md b/docs/en/framework/architecture/best-practices/repositories.md index 5e491ecb80..227c620f45 100644 --- a/docs/en/framework/architecture/best-practices/repositories.md +++ b/docs/en/framework/architecture/best-practices/repositories.md @@ -1,6 +1,10 @@ # Repository Best Practices & Conventions -### Repository Interfaces +> This document offers best practices for implementing Repository classes in your modules and applications based on Domain-Driven-Design principles. +> +> **Ensure you've read the [*Repositories*](../domain-driven-design/repositories.md) document first.** + +## Repository Interfaces * **Do** define repository interfaces in the **domain layer**. * **Do** define a repository interface (like `IIdentityUserRepository`) and create its corresponding implementations for **each aggregate root**. @@ -30,7 +34,7 @@ public interface IIdentityUserRepository : IBasicRepository * **Do** inherit the repository interface from `IBasicRepository` (as normally) or a lower-featured interface, like `IReadOnlyRepository` (if it's needed). * **Do not** define repositories for entities those are **not aggregate roots**. -### Repository Methods +## Repository Methods * **Do** define all repository methods as **asynchronous**. * **Do** add an **optional** `cancellationToken` parameter to every method of the repository. Example: @@ -68,7 +72,7 @@ Task> GetListByNormalizedRoleNameAsync( * **Avoid** to create projection classes for entities to get less property of an entity from the repository. Example: Avoid to create BasicUserView class to select a few properties needed for the use case needs. Instead, directly use the aggregate root class. However, there may be some exceptions for this rule, where: * Performance is so critical for the use case and getting the whole aggregate root highly impacts the performance. -### See Also +## See Also * [Entity Framework Core Integration](./entity-framework-core-integration.md) * [MongoDB Integration](./mongodb-integration.md) diff --git a/docs/en/framework/architecture/domain-driven-design/data-transfer-objects.md b/docs/en/framework/architecture/domain-driven-design/data-transfer-objects.md index e33e7895ca..8816411a5b 100644 --- a/docs/en/framework/architecture/domain-driven-design/data-transfer-objects.md +++ b/docs/en/framework/architecture/domain-driven-design/data-transfer-objects.md @@ -1,7 +1,5 @@ # Data Transfer Objects -## Introduction - **Data Transfer Objects** (DTO) are used to transfer data between the **Application Layer** and the **Presentation Layer** or other type of clients. Typically, an [application service](./application-services.md) is called from the presentation layer (optionally) with a **DTO** as the parameter. It uses domain objects to **perform some specific business logic** and (optionally) returns a DTO back to the presentation layer. Thus, the presentation layer is completely **isolated** from domain layer. diff --git a/docs/en/framework/architecture/domain-driven-design/domain-services.md b/docs/en/framework/architecture/domain-driven-design/domain-services.md index 358e850527..36bd732a3c 100644 --- a/docs/en/framework/architecture/domain-driven-design/domain-services.md +++ b/docs/en/framework/architecture/domain-driven-design/domain-services.md @@ -1,7 +1,5 @@ # Domain Services -## Introduction - In a [Domain Driven Design](../domain-driven-design) (DDD) solution, the core business logic is generally implemented in aggregates ([entities](./entities.md)) and the Domain Services. Creating a Domain Service is especially needed when; * You implement a core domain logic that depends on some services (like repositories or other external services). diff --git a/docs/en/framework/fundamentals/caching.md b/docs/en/framework/fundamentals/caching.md index 9b29763737..bdef675f64 100644 --- a/docs/en/framework/fundamentals/caching.md +++ b/docs/en/framework/fundamentals/caching.md @@ -1,14 +1,14 @@ # Distributed Caching -ABP extends the [ASP.NET Core distributed cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). +ABP extends the [ASP.NET Core distributed cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) to provide a more comfortable and easy-to-use cache service. -> **Default implementation of the `IDistributedCache` interface is` MemoryDistributedCache` which works in-memory.** See [ASP.NET Core's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) to see how to switch to Redis or another cache provider. Also, see the [Redis Cache](./redis-cache.md) document if you want to use Redis as the distributed cache server. +> **Default implementation of the `IDistributedCache` interface is` MemoryDistributedCache` which works in-memory.** Memory cache is only useful if you are building a monolith application and you run a single instance of your application. For other cases, consider using a distributed cache server. See the ***[When to Use a Distributed Cache Server](../../kb/when-to-use-a-distributed-cache-server.md)*** document for more details. ## Installation -> This package is already installed by default with the [application startup template](../../solution-templates/layered-web-application). So, most of the time, you don't need to install it manually. +> This package is already installed by default in [startup templates](../../solution-templates/index.md). So, most of the time, you don't need to install it manually. -[Volo.Abp.Caching](https://www.nuget.org/packages/Volo.Abp.Caching) is the main package of the caching system. You can install it a project using the add-package command of the [ABP CLI](../../cli): +[Volo.Abp.Caching](https://www.nuget.org/packages/Volo.Abp.Caching) is the main package of the caching system. You can install it as a project using the add-package command of the [ABP CLI](../../cli): ```bash abp add-package Volo.Abp.Caching @@ -24,10 +24,10 @@ ASP.NET Core defines the `IDistributedCache` interface to get/set the cache valu * It works with **byte arrays** rather than .NET objects. So, you need to **serialize/deserialize** the objects you need to cache. * It provides a **single key pool** for all cache items, so; - * You need to care about the keys to distinguish **different type of objects**. + * You need to care about the keys to distinguish **different types of objects**. * You need to care about the cache items of **different tenants** in a [multi-tenant](../architecture/multi-tenancy) system. -> `IDistributedCache` is defined in the `Microsoft.Extensions.Caching.Abstractions` package. That means it is not only usable for ASP.NET Core applications, but also available to **any type of applications**. +> `IDistributedCache` is defined in the `Microsoft.Extensions.Caching.Abstractions` package. That means it is not only usable for ASP.NET Core applications but also available to **any type of applications**. See [ASP.NET Core's distributed caching document](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) for more information. @@ -37,12 +37,12 @@ ABP defines the generic `IDistributedCache` interface in the [Volo.A `IDistributedCache` solves the difficulties explained above; -* It internally **serializes/deserializes** the cached objects. Uses **JSON** serialization by default, but can be overridden by replacing the `IDistributedCacheSerializer` service in the [dependency injection](./dependency-injection.md) system. -* It automatically adds a **cache name** prefix to the cache keys based on the object type stored in the cache. Default cache name is the full name of the cache item class (`CacheItem` postfix is removed if your cache item class ends with it). You can use the **`CacheName` attribute** on the cache item class to set the cache name. +* It internally **serializes/deserializes** the cached objects. It uses **JSON** serialization by default but can be overridden by replacing the `IDistributedCacheSerializer` service in the [dependency injection](./dependency-injection.md) system. +* It automatically adds a **cache name** prefix to the cache keys based on the object type stored in the cache. The default cache name is the full name of the cache item class (`CacheItem` postfix is removed if your cache item class ends with it). You can use the **`CacheName` attribute** on the cache item class to set the cache name. * It automatically adds the **current tenant id** to the cache key to distinguish cache items for different tenants (if your application is [multi-tenant](../architecture/multi-tenancy)). Define `IgnoreMultiTenancy` attribute on the cache item class to disable this if you want to share the cached objects among all tenants in a multi-tenant application. -* Allows to define a **global cache key prefix** per application, so different applications can use their isolated key pools in a shared distributed cache server. +* Allows defining a **global cache key prefix** per application so different applications can use their isolated key pools in a shared distributed cache server. * It **can tolerate errors** wherever possible and bypasses the cache. This is useful when you have temporary problems on the cache server. -* It has methods like `GetManyAsync` and `SetManyAsync` which significantly improve the performance on **batch operations**. +* It has methods like `GetManyAsync` and `SetManyAsync` which significantly improve the performance of **batch operations**. **Example: Store Book names and prices in the cache** @@ -167,7 +167,7 @@ namespace MyProject ```` * This sample service uses the `GetOrAddAsync()` method to get a book item from the cache. -* Since cache explicitly implemented as using `Guid` as cache key, `Guid` value passed to `_cache_GetOrAddAsync()` method. +* Since the cache is explicitly implemented as using `Guid` as the cache key, the `Guid` value is passed to the `_cache_GetOrAddAsync()` method. #### Complex Types as the Cache Key @@ -228,29 +228,29 @@ Configure(options => * `HideErrors` (`bool`, default: `true`): Enables/disables hiding the errors on writing/reading values from the cache server. * `KeyPrefix` (`string`, default: `null`): If your cache server is shared by multiple applications, you can set a prefix for the cache keys for your application. In this case, different applications can not overwrite each other's cache items. -* `GlobalCacheEntryOptions` (`DistributedCacheEntryOptions`): Used to set default distributed cache options (like `AbsoluteExpiration` and `SlidingExpiration`) used when you don't specify the options while saving cache items. Default value uses the `SlidingExpiration` as 20 minutes. +* `GlobalCacheEntryOptions` (`DistributedCacheEntryOptions`): Used to set default distributed cache options (like `AbsoluteExpiration` and `SlidingExpiration`) used when you don't specify the options while saving cache items. The default value uses the `SlidingExpiration` as 20 minutes. ## Error Handling -When you design a cache for your objects, you typically try to get the value from cache first. If not found in the cache, you query the object from the **original source**. It may be located in a **database** or may require to perform an HTTP call to a remote server. +When you design a cache for your objects, you typically try to get the value from the cache first. If not found in the cache, you query the object from the **original source**. It may be located in a **database** or may require an HTTP call to a remote server to be performed. -In most cases, you want to **tolerate the cache errors**; If you get error from the cache server you don't want to cancel the operation. Instead, you silently hide (and log) the error and **query from the original source**. This is what the ABP does by default. +In most cases, you want to **tolerate the cache errors**; If you get an error from the cache server, you don't want to cancel the operation. Instead, you silently hide (and log) the error and **query from the original source**. This is what the ABP does by default. ABP's Distributed Cache [handle](./exception-handling.md), log and hide errors by default. There is an option to change this globally (see the options below). -In addition, all of the `IDistributedCache` (and `IDistributedCache`) methods have an optional `hideErrors` parameter, which is `null` by default. The global value is used if this parameter left as `null`, otherwise you can decide to hide or throw the exceptions for individual method calls. +In addition, all of the `IDistributedCache` (and `IDistributedCache`) methods have an optional `hideErrors` parameter, which is `null` by default. The global value is used if this parameter is left as `null`; otherwise, you can decide to hide or throw the exceptions for individual method calls. ## Batch Operations -ABP's distributed cache interfaces provide methods to perform batch methods those improves the performance when you want to batch operation multiple cache items in a single method call. +ABP's distributed cache interfaces provide methods to perform batch operations that improve performance when you want to batch operation multiple cache items in a single method call. * `SetManyAsync` and `SetMany` methods can be used to set multiple values to the cache. * `GetManyAsync` and `GetMany` methods can be used to retrieve multiple values from the cache. * `GetOrAddManyAsync` and `GetOrAddMany` methods can be used to retrieve multiple values and set missing values from the cache -* `RefreshManyAsync` and `RefreshMany` methods can be used to resets the sliding expiration timeout of multiple values from the cache +* `RefreshManyAsync` and `RefreshMany` methods can be used to reset the sliding expiration timeout of multiple values from the cache * `RemoveManyAsync` and `RemoveMany` methods can be used to remove multiple values from the cache -> These are not standard methods of the ASP.NET Core caching. So, some providers may not support them. They are supported by the [ABP Redis Cache integration package](./redis-cache.md). If the provider doesn't support, it fallbacks to `SetAsync` and `GetAsync` ... methods (called once for each item). +> These are not standard methods of the ASP.NET Core caching. So, some providers may not support them. They are supported by the [ABP Redis Cache integration package](./redis-cache.md). If the provider doesn't support it, it falls back to `SetAsync` and `GetAsync` ... methods (called once for each item). ## Caching Entities @@ -266,17 +266,17 @@ It's designed as read-only and automatically invalidates a cached entity if the Distributed cache service provides an interesting feature. Assume that you've updated the price of a book in the database, then set the new price to the cache, so you can use the cached value later. What if you have an exception after setting the cache and you **rollback the transaction** that updates the price of the book? In this case, cache value will be incorrect. -`IDistributedCache<..>` methods gets an optional parameter, named `considerUow`, which is `false` by default. If you set it to `true`, then the changes you made for the cache are not actually applied to the real cache store, but associated with the current [unit of work](../architecture/domain-driven-design/unit-of-work.md). You get the value you set in the same unit of work, but the changes are applied **only if the current unit of work succeed**. +`IDistributedCache<..>` methods gets an optional parameter, named `considerUow`, which is `false` by default. If you set it to `true`, then the changes you made for the cache are not actually applied to the real cache store, but associated with the current [unit of work](../architecture/domain-driven-design/unit-of-work.md). You get the value you set in the same unit of work, but the changes are applied **only if the current unit of work succeeds**. ### IDistributedCacheSerializer -`IDistributedCacheSerializer` service is used to serialize and deserialize the cache items. Default implementation is the `Utf8JsonDistributedCacheSerializer` class that uses `IJsonSerializer` service to convert objects to [JSON](../../json-serialization.md) and vice verse. Then it uses UTC8 encoding to convert the JSON string to a byte array which is accepted by the distributed cache. +`IDistributedCacheSerializer` service is used to serialize and deserialize the cache items. The default implementation is the `Utf8JsonDistributedCacheSerializer` class that uses `IJsonSerializer` service to convert objects to [JSON](../../json-serialization.md) and vice verse. Then it uses UTC8 encoding to convert the JSON string to a byte array which is accepted by the distributed cache. -You can [replace](./dependency-injection.md) this service by your own implementation if you want to implement your own serialization logic. +You can [replace](./dependency-injection.md) this service with your own implementation if you want to implement your own serialization logic. ### IDistributedCacheKeyNormalizer -`IDistributedCacheKeyNormalizer` is implemented by the `DistributedCacheKeyNormalizer` class by default. It adds cache name, application cache prefix and current tenant id to the cache key. If you need a more advanced key normalization, you can [replace](./dependency-injection.md) this service by your own implementation. +`IDistributedCacheKeyNormalizer` is implemented by the `DistributedCacheKeyNormalizer` class by default. It adds the cache name, application cache prefix and current tenant ID to the cache key. If you need a more advanced key normalization, you can [replace](./dependency-injection.md) this service with your own implementation. ## See Also diff --git a/docs/en/framework/infrastructure/background-jobs/index.md b/docs/en/framework/infrastructure/background-jobs/index.md index c4536885c7..35f761ddf1 100644 --- a/docs/en/framework/infrastructure/background-jobs/index.md +++ b/docs/en/framework/infrastructure/background-jobs/index.md @@ -221,6 +221,13 @@ public class MyModule : AbpModule } ```` +* `JobPollPeriod` is used to determine the interval between two job polling operations. Default is 5000 ms (5 seconds). +* `MaxJobFetchCount` is used to determine the maximum job count to fetch in a single polling operation. Default is 1000. +* `DefaultFirstWaitDuration` is used to determine the duration to wait before the first retry. Default is 60 seconds. +* `DefaultTimeout` is used to determine the timeout duration for a job. Default is 172800 seconds (2 days). +* `DefaultWaitFactor` is used to determine the factor to increase the wait duration between retries. Default is 2.0. +* `DistributedLockName` is used to determine the distributed lock name to use. Default is `AbpBackgroundJobWorker`. + ### Data Store The default background job manager needs a data store to save and read jobs. It defines `IBackgroundJobStore` as an abstraction to store the jobs. diff --git a/docs/en/framework/infrastructure/features.md b/docs/en/framework/infrastructure/features.md index 06bd99a223..ef5d97bd91 100644 --- a/docs/en/framework/infrastructure/features.md +++ b/docs/en/framework/infrastructure/features.md @@ -161,8 +161,16 @@ namespace FeaturesDemo { var myGroup = context.AddGroup("MyApp"); - myGroup.AddFeature("MyApp.PdfReporting", defaultValue: "false"); - myGroup.AddFeature("MyApp.MaxProductCount", defaultValue: "10"); + myGroup.AddFeature( + "MyApp.PdfReporting", + defaultValue: "false" + ); + + myGroup.AddFeature( + "MyApp.MaxProductCount", + defaultValue: "10", + valueType: new FreeTextStringValueType(new NumericValueValidator()) + ); } } } diff --git a/docs/en/framework/ui/blazor/global-scripts-styles.md b/docs/en/framework/ui/blazor/global-scripts-styles.md index d53fc3d2ba..a8819df35a 100644 --- a/docs/en/framework/ui/blazor/global-scripts-styles.md +++ b/docs/en/framework/ui/blazor/global-scripts-styles.md @@ -1,83 +1,72 @@ # Blazor UI: Managing Global Scripts & Styles -Some modules may require additional styles or scripts that need to be referenced in **index.html** file. It's not easy to find and update these types of references in Blazor apps. ABP offers a simple, powerful, and modular way to manage global style and scripts in Blazor apps. +You can add your JavaScript and CSS files from your modules or applications to the Blazor global assets system. All the JavaScript and CSS files will be added to the `global.js` and `global.css` files. You can access these files via the following URL in a Blazor WASM project: -To update script & style references without worrying about dependencies, ordering, etc in a project, you can use the [bundle command](../../../cli#bundle). +- https://localhost/global.js +- https://localhost/global.css -You can also add custom styles and scripts and let ABP manage them for you. In your Blazor project, you can create a class implementing `IBundleContributor` interface. +## Add JavaScript and CSS to the global assets system in the module -`IBundleContributor` interface contains two methods. +Your module project solution will have two related Blazor projects: -* `AddScripts(...)` -* `AddStyles(...)` +* `MyModule.Blazor`๏ผšThis project includes the JavaScript/CSS files required for your Blazor components. The `MyApp.Blazor.Client (Blazor WASM)` project will reference this project. +* `MyModule.Blazor.WebAssembly.Bundling`๏ผšThis project is used to add your JavaScript/CSS files to the Blazor global resources. The `MyModule.Blazor (ASP.NET Core)` project will reference this project. -Both methods get `BundleContext` as a parameter. You can add scripts and styles to the `BundleContext` and run [bundle command](../../../cli#bundle). Bundle command detects custom styles and scripts with module dependencies and updates `index.html` file. +You need to define JavaScript and CSS contributor classes in the `MyModule.Blazor.WebAssembly.Bundling` project to add the files to the global assets system. -## Example Usage -```csharp -namespace MyProject.Blazor +> Please use `BlazorWebAssemblyStandardBundles.Scripts.Global` and `BlazorWebAssemblyStandardBundles.Styles.Global` for the bundle name. + +```cs +public class MyModuleBundleScriptContributor : BundleContributor { - public class MyProjectBundleContributor : IBundleContributor + public override void ConfigureBundle(BundleConfigurationContext context) { - public void AddScripts(BundleContext context) - { - context.Add("site.js"); - } - - public void AddStyles(BundleContext context) - { - context.Add("main.css"); - context.Add("custom-styles.css"); - } + context.Files.AddIfNotContains("_content/MyModule.Blazor/libs/myscript.js"); } } ``` -> There is a BundleContributor class implementing `IBundleContributor` interface coming by default with the startup templates. So, most of the time, you don't need to add it manually. - -## Bundling And Minification -`abp bundle` command offers bundling and minification support for client-side resources(JavaScript and CSS files). `abp bundle` command reads the `appsettings.json` file inside the Blazor project and bundles the resources according to the configuration. You can find the bundle configurations inside `AbpCli.Bundle` element. - -Here are the options that you can control inside the `appsettings.json` file. - -`Mode`: Bundling and minification mode. Possible values are -* `BundleAndMinify`: Bundle all the files into a single file and minify the content. -* `Bundle`: Bundle all files into a single file, but not minify. -* `None`: Add files individually, do not bundle. - -`Name`: Bundle file name. Default value is `global`. - -`Parameters`: You can define additional key/value pair parameters inside this section. `abp bundle` command automatically sends these parameters to the bundle contributors, and you can check these parameters inside the bundle contributor, take some actions according to these values. - -Let's say that you want to exclude some resources from the bundle and control this action using the bundle parameters. You can add a parameter to the bundle section like below. - -```json -"AbpCli": { - "Bundle": { - "Mode": "BundleAndMinify", /* Options: None, Bundle, BundleAndMinify */ - "Name": "global", - "Parameters": { - "ExcludeThemeFromBundle":"true" - } - } - } -``` - -You can check this parameter and take action like below. - -```csharp -public class MyProjectNameBundleContributor : IBundleContributor +```cs +public class MyModuleBundleStyleContributor : BundleContributor { - public void AddScripts(BundleContext context) + public override void ConfigureBundle(BundleConfigurationContext context) { + context.Files.AddIfNotContains("_content/MyModule.Blazor/libs/mystyle.css"); } +} +``` - public void AddStyles(BundleContext context) +```cs +[DependsOn( + typeof(AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule) +)] +public class MyBlazorWebAssemblyBundlingModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) { - var excludeThemeFromBundle = bool.Parse(context.Parameters.GetValueOrDefault("ExcludeThemeFromBundle")); - context.Add("mytheme.css", excludeFromBundle: excludeThemeFromBundle); - context.Add("main.css"); + Configure(options => + { + // Add script bundle + options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global) + .AddContributors(typeof(MyModuleBundleScriptContributor)); + + // Add style bundle + options.StyleBundles.Get(BlazorWebAssemblyStandardBundles.Styles.Global) + .AddContributors(typeof(MyModuleBundleStyleContributor)); + }); } } ``` +## Add JavaScript and CSS to the global assets system in the application + +This is similar to the module. You need to define JavaScript and CSS contributor classes in the `MyApp.Blazor.Client` project to add the files to the global assets system. + +## AbpBundlingGlobalAssetsOptions + +You can configure the JavaScript and CSS file names in the `GlobalAssets` property of the `AbpBundlingOptions` class. The default values are `global.js` and `global.css`. + +## Reference + +- [ASP.NET Core MVC Bundling & Minification](../mvc-razor-pages/bundling-minification#bundle-contributorsg) +- [ABP Global Assets - New way to bundle JavaScript/CSS files in Blazor WebAssembly app](https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2024-11-25-Global-Assets/POST.md) diff --git a/docs/en/framework/ui/mvc-razor-pages/tag-helpers/dynamic-forms.md b/docs/en/framework/ui/mvc-razor-pages/tag-helpers/dynamic-forms.md index ceab5d1312..6b9a07f63a 100644 --- a/docs/en/framework/ui/mvc-razor-pages/tag-helpers/dynamic-forms.md +++ b/docs/en/framework/ui/mvc-razor-pages/tag-helpers/dynamic-forms.md @@ -23,7 +23,7 @@ public class DynamicFormsModel : PageModel new SelectListItem { Value = "CA", Text = "Canada"}, new SelectListItem { Value = "US", Text = "USA"}, new SelectListItem { Value = "UK", Text = "United Kingdom"}, - new SelectListItem { Value = "RU", Text = "Russia"} + new SelectListItem { Value = "RU", Text = "Turkey"} }; public void OnGet() @@ -217,7 +217,7 @@ public class DynamicFormsModel : PageModel new SelectListItem { Value = "CA", Text = "Canada"}, new SelectListItem { Value = "US", Text = "USA"}, new SelectListItem { Value = "UK", Text = "United Kingdom"}, - new SelectListItem { Value = "RU", Text = "Russia"} + new SelectListItem { Value = "RU", Text = "Turkey"} }; public void OnGet() @@ -278,4 +278,4 @@ public string Name { get; set; } ## See Also -* [Form Elements](form-elements.md) \ No newline at end of file +* [Form Elements](form-elements.md) diff --git a/docs/en/get-started/layered-web-application.md b/docs/en/get-started/layered-web-application.md index f35b1c39db..8d4e540292 100644 --- a/docs/en/get-started/layered-web-application.md +++ b/docs/en/get-started/layered-web-application.md @@ -13,31 +13,15 @@ In this quick start guide, you will learn how to create and run a layered (and p ## Setup your development environment -First things first! Let's setup your development environment before creating the first project. +First things first! Let's setup your development environment before creating the first project. The following tools should be installed on your development machine: -### Pre-requirements +* [Visual Studio 2022](https://visualstudio.microsoft.com/) or another IDE that supports [.NET 9.0+](https://dotnet.microsoft.com/download/dotnet) development. +* [.NET 9.0+](https://dotnet.microsoft.com/en-us/download/dotnet){{ if UI != "Blazor" }} +* [Node v22.11+](https://nodejs.org/) +* [Yarn v1.22+ (not v2+)](https://classic.yarnpkg.com/en/docs/install) or npm v10+ (already installed with Node){{ end }} +* [Docker Desktop](https://www.docker.com/products/docker-desktop/) -The following tools should be installed on your development machine: - -* [Visual Studio 2022](https://visualstudio.microsoft.com/) or another IDE that supports [.NET 9.0+](https://dotnet.microsoft.com/download/dotnet) development. [1](#f-editor) -* [.NET 9.0+](https://dotnet.microsoft.com/en-us/download/dotnet) -{{ if UI != "Blazor" }} -* [Node v20.11+](https://nodejs.org/) -* [Yarn v1.22+ (not v2)](https://classic.yarnpkg.com/en/docs/install) [2](#f-yarn) or npm v10+ (already installed with Node) -{{ end }} -{{ if Tiered == "Yes" }} -* [Redis](https://redis.io/) (as the [distributed cache](../framework/fundamentals/caching.md)). -{{ else }} -* [Redis](https://redis.io/) (as the [distributed cache](../framework/fundamentals/caching.md)) is required if you select the Public website option. -{{ end }} - -1 _You can use another editor instead of Visual Studio as long as it supports .NET Core and ASP.NET Core._ [โ†ฉ](#a-editor) - -{{ if UI != "Blazor" }} - -2 _Yarn v2 works differently and is not supported._ [โ†ฉ](#a-yarn) - -{{ end }} +> Check the [Pre-requirements document](pre-requirements.md) for more detailed information about these tools. ## Creating a New Solution @@ -147,7 +131,7 @@ In the Solution Runner section (on the left side) you can see all the runnable a You can run all the applications or start them one by one. To start an application, either click the *Play* icon near to the application or right-click and select the *Run* -> *Start* context menu item. -> For the first run, you'll need to build the application. You can achieve this by selecting *Run* -> *Build & Start* from the context menu. +> ABP Studio builds the application by default. So, you don't need to manually build the application before running it. You can start the following application(s): @@ -229,7 +213,7 @@ You can start the following application(s): {{ else }} - `Acme.BookStore.Web` {{ end }} - + Before starting the mobile application, ensure that you configure it for [react-native](../framework/ui/react-native) or [MAUI](../framework/ui/maui). ![mobile-sample](images/abp-studio-mobile-sample.gif) diff --git a/docs/en/get-started/microservice.md b/docs/en/get-started/microservice.md index 24fc745fe0..e29c4cbe10 100644 --- a/docs/en/get-started/microservice.md +++ b/docs/en/get-started/microservice.md @@ -4,6 +4,21 @@ In this quick start guide, you will learn how to create and run a microservice solution using [ABP Studio](../studio/index.md). +## Setup your development environment + +First things first! Let's setup your development environment before creating the first project. The following tools should be installed on your development machine: + +* [Visual Studio 2022](https://visualstudio.microsoft.com/vs/) or another IDE that supports .NET development +* [.NET 9.0+](https://dotnet.microsoft.com/en-us/download/dotnet) +* [Node v22.11+](https://nodejs.org/) +* [Yarn v1.22+ (not v2+)](https://classic.yarnpkg.com/en/docs/install) or npm v10+ (already installed with Node) +* [Docker Desktop (with Kubernetes enabled)](https://www.docker.com/products/docker-desktop/) +* [Helm](https://helm.sh/docs/intro/install/) +* [NGINX Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/) +* [mkcert](https://github.com/FiloSottile/mkcert#installation) + +> Check the [Pre-requirements document](pre-requirements.md) for more detailed information about these tools. + ## Creating a New Solution > ๐Ÿ›ˆ This document uses [ABP Studio](../studio/index.md) to create new ABP solutions. **ABP Studio** is in the beta version now. If you have any issues, you can use the [ABP CLI](../cli/index.md) to create new solutions. You can also use the [getting started page](https://abp.io/get-started) to easily build ABP CLI commands for new project creations. @@ -127,13 +142,11 @@ In the *Solution Runner* section (on the left side) you can see all the runnable As shown in the figure above, the executable applications are grouped into folders like `apps`, `gateways`, `infrastructure`, and `services`. You can start/stop them all, a group (folder) of them, or one by one. -Before running the applications, it is good to be sure that all applications are built. To do that, right-click the root item in the *Solution Runner* and select *Build* -> *Build All* action. - -![abp-studio-microservice-solution-runner-build-all](images/abp-studio-microservice-solution-runner-build-all.png) +Before running the applications, you can run the all application by right-clicking the root item in the *Solution Runner* and select *Build* -> *Build All* action. However, you don't need to do that, because ABP Studio builds the applications before running them by default. -> *Solution Runner* doesn't build an application before running it. That provides a great performance gain because most of the time you will work on one or a few services and you don't need to build all of the other applications in every run. However, if you want to build before running, you can right-click an item in the *Solution Runner* tree and select *Run* -> *Build & Start* command. +> If you want to change this behavior, and don't want ABP Studio to build before running the applications, you can click the *Manage start actions* button in the *Solution Runner*, which you can see from the root item or per folder. -It will take some time to build all. Once all is done, you can start the system. You can click the *Play* button on the root item in Solution Runner to start all the applications. +You can click the *Play* button on the root item in *Solution Runner* to start all the applications. > **About the Docker Containers** > diff --git a/docs/en/get-started/pre-requirements.md b/docs/en/get-started/pre-requirements.md new file mode 100644 index 0000000000..c1c7b39c28 --- /dev/null +++ b/docs/en/get-started/pre-requirements.md @@ -0,0 +1,107 @@ +# Prerequisites for Developing ABP Applications + +This document will guide you through preparing your development environment for ABP based application development. + +## Notices + +The prerequisites mentioned in this document are not necessary for every project type; + +* You don't need to install the EF Core CLI if your application uses MongoDB instead of EF Core. +* You don't need to install Helm, NGINX Ingress, or mkcert if you are developing a non-microservice application. + +`README.MD` files in new solutions contain specific requirements for your solution. Please refer to the `README.MD` file of your solution. + +## IDE + +You need to use an IDE that supports .NET development. The following IDEs are the most popular ones for .NET development. + +### Visual Studio + +Visual Studio is Microsoft's IDE and is the de facto tool for developing .NET projects. You can download Visual Studio from the [Visual Studio official website](https://visualstudio.microsoft.com/). It also has a **free Community edition** which is more than enough for ABP projects. + +### Visual Studio Code + +Visual Studio Code is a **free and cross-platform** lightweight code editor that supports .NET development. You can [download from here](https://code.visualstudio.com/download). + +### JetBrains Rider + +[JetBrains Rider](https://www.jetbrains.com/rider/download) is a cross-platform IDE by [JetBrains](https://www.jetbrains.com/) that supports .NET development. It is **[free for non-commercial use](https://blog.jetbrains.com/blog/2024/10/24/webstorm-and-rider-are-now-free-for-non-commercial-use/)**. + +## .NET SDK + +ABP is based on NET, so you need to install the .NET SDK. You can download the .NET SDK from the [.NET official website](https://dotnet.microsoft.com/en-us/download/dotnet/9.0). + +> Installing Visual Studio or JetBrains Rider may automatically install the .NET SDK. + +### EF Core CLI + +If you are using [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/) as your database access provider, you need to install the [EF Core CLI](https://learn.microsoft.com/en-us/ef/core/cli/dotnet). You can install it by running the following command: + +```bash +dotnet tool install --global dotnet-ef +``` + +If you have already installed the `EF Core CLI`, you can update it by running the following command: + +```bash +dotnet tool update --global dotnet-ef +``` + +## Node.js and Yarn + +ABP projects include some frontend resource packages, so you need to install Node.js and Yarn to manage these resource packages. You can download Node.js from the [official Node.js website](https://nodejs.org/). We recommend installing version v20.11+. + +Using Yarn (classic) to manage frontend resource packages is faster and more stable than using npm. You can download `Yarn` from the [Yarn official website](https://classic.yarnpkg.com/en/docs/install). We recommend installing Yarn v1.22+ (make sure to install the Classic version, not v2+). + +To install Yarn using npm, run the following command: + +```bash +npm install --global yarn +``` + +## Docker Engine or Docker Desktop + +ABP's [Layered Solution](../solution-templates/layered-web-application/index.md) and [Microservice Solution](../solution-templates/microservice/index.md) use Docker to run infrastructure services (e.g. SQL Server, Redis, RabbitMQ) required by your application. You can install Docker Engine or Docker Desktop (recommended) on Windows, macOS and Linux. + +* [Docker Desktop](https://www.docker.com/products/docker-desktop/) (recommended) +* [Docker Engine](https://docs.docker.com/engine/install/) + +### Is Docker Engine or Docker Desktop Free? + +Docker Engine is an open-source and free containerization technology for building and containerizing your applications. [`Docker Engine` follows the Apache License 2.0](https://docs.docker.com/engine/#licensing). + +Docker Desktop is free [for small businesses (fewer than 250 employees and less than $10 million in annual revenue), personal use, education, and non-commercial open-source projects](https://docs.docker.com/subscription/desktop-license/). + +## PowerShell + +ABP startup solution templates and tools use some PowerShell scripts (`*.ps1`) to perform certain tasks. You can refer to the [PowerShell documentation](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell) for guidance on how to install PowerShell on Windows, macOS, and Linux. + +* [Install PowerShell on Windows](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows) +* [Install PowerShell on macOS](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-macos) +* [Install PowerShell on Linux](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-linux) + +## MicroService Solution + +The following tools are only required to develop ABP's [microservice solution](../solution-templates/microservice/index.md) + +### Helm + +[Helm](https://helm.sh/) is a package manager for Kubernetes. You can install Helm by following the [Helm installation guide](https://helm.sh/docs/intro/install/). + +See [Helm Deployment on Local Kubernetes Cluster](../solution-templates/microservice/helm-charts-and-kubernetes.md) for more information. + +### NGINX Ingress or NGINX Ingress using Helm + +[NGINX Ingress](https://kubernetes.github.io/ingress-nginx/deploy/) is an Ingress controller for Kubernetes. You can install NGINX Ingress by following the [NGINX Ingress installation guide](https://kubernetes.github.io/ingress-nginx/deploy/). + +If you are using Helm, you can install NGINX Ingress using the following commands: + +```cs +helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx +helm repo update +helm upgrade --install --version=4.0.19 ingress-nginx ingress-nginx/ingress-nginx +``` + +### mkcert + +Use mkcert to generate trusted certificates for local development. You can install mkcert by following the [official mkcert installation guide](https://github.com/FiloSottile/mkcert#installation). diff --git a/docs/en/get-started/single-layer-web-application.md b/docs/en/get-started/single-layer-web-application.md index 9bcf2e0e7e..67d840d768 100644 --- a/docs/en/get-started/single-layer-web-application.md +++ b/docs/en/get-started/single-layer-web-application.md @@ -12,31 +12,14 @@ In this quick start guide, you will learn how to create and run a single layer w ## Setup your development environment -First things first! Let's setup your development environment before creating the first project. +First things first! Let's setup your development environment before creating the first project. The following tools should be installed on your development machine: -### Pre-requirements +* [Visual Studio 2022](https://visualstudio.microsoft.com/) or another IDE that supports [.NET 9.0+](https://dotnet.microsoft.com/download/dotnet) development. +* [.NET 9.0+](https://dotnet.microsoft.com/en-us/download/dotnet){{ if UI != "Blazor" }} +* [Node v22.11+](https://nodejs.org/) +* [Yarn v1.22+ (not v2+)](https://classic.yarnpkg.com/en/docs/install) or npm v10+ (already installed with Node){{ end }} -The following tools should be installed on your development machine: - -* [Visual Studio 2022](https://visualstudio.microsoft.com/) or another IDE that supports [.NET 9.0+](https://dotnet.microsoft.com/download/dotnet) development. [1](#f-editor) -* [.NET 9.0+](https://dotnet.microsoft.com/en-us/download/dotnet) -{{ if UI != "Blazor" }} -* [Node v20.11+](https://nodejs.org/) -* [Yarn v1.22+ (not v2)](https://classic.yarnpkg.com/en/docs/install) [2](#f-yarn) or npm v10+ (already installed with Node) -{{ end }} -{{ if Tiered == "Yes" }} -* [Redis](https://redis.io/) (as the [distributed cache](../framework/fundamentals/caching.md)). -{{ else }} -* [Redis](https://redis.io/) (as the [distributed cache](../framework/fundamentals/caching.md)) is required if you select the Public website option. -{{ end }} - -1 _You can use another editor instead of Visual Studio as long as it supports .NET Core and ASP.NET Core._ [โ†ฉ](#a-editor) - -{{ if UI != "Blazor" }} - -2 _Yarn v2 works differently and is not supported._ [โ†ฉ](#a-yarn) - -{{ end }} +> Check the [Pre-requirements document](pre-requirements.md) for more detailed information about these tools. ## Creating a New Solution @@ -116,8 +99,6 @@ In the Solution Runner section (on the left side) you can see all the runnable a To start an application, either click the *Play* icon near to the application or right-click and select the *Run* -> *Start* context menu item. -> For the first run, you'll need to build the application. You can achieve this by selecting *Run* -> *Build & Start* from the context menu. - You can start the `Acme.BookStore`{{ if UI == "NG" }} and `Acme.BookStore.Angular`{{ end }}. Once the `Acme.BookStore{{ if UI == "NG" }}.Angular{{ end }}` application started, you can right-click it and select the *Browse* command: diff --git a/docs/en/kb/index.md b/docs/en/kb/index.md new file mode 100644 index 0000000000..8a497cdeee --- /dev/null +++ b/docs/en/kb/index.md @@ -0,0 +1,6 @@ +# ABP Knowledge Base + +The following documents provide useful information about several topics you might need to know. + +* [How to Fix "Filename too long" Error on Windows](windows-path-too-long-fix.md) +* [When to Use a Distributed Cache Server](when-to-use-a-distributed-cache-server.md) \ No newline at end of file diff --git a/docs/en/kb/when-to-use-a-distributed-cache-server.md b/docs/en/kb/when-to-use-a-distributed-cache-server.md new file mode 100644 index 0000000000..a414e5cd78 --- /dev/null +++ b/docs/en/kb/when-to-use-a-distributed-cache-server.md @@ -0,0 +1,49 @@ +# When to Use a Distributed Cache Server + +ABP provides a [distributed cache service](../framework/fundamentals/caching.md) that is based on [ASP.NET Core's distributed cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed). This document explains when you need to have a separate cache server for your applications. + +## Understanding the Default Cache Service + +**Default implementation of the cache service works in-memory**. Memory cache is only useful if you are building a monolith application and you run a single instance of your application. For other cases, **you should use a real distributed cache server**. + +Here are a few example cases where you should use a distributed cache server: + +* You have a **monolith application**, but you run **multiple instances** of that application concurrently, for example, in a [clustered environment](../deployment/clustered-environment.md) +* You build a **microservice** or any kind of **distributed** system +* You have web **multiple applications** in your solution and they should share the same cache + +The problem is obvious: If each application instance uses its internal in-memory cache, and if two or more applications cache the same data, it is probable that they will cache different copies of the data. In that case, there is no way to **invalidate/refresh** that data in every application's memory when the data changes. + +## What is a Distributed Cache Server + +A **distributed cache server** (e.g. [Redis](../framework/fundamentals/redis-cache.md)) stores cache objects in a separate server application and allows multiple applications/processes to share the same cache objects. In that way; + +* All applications/services and all their instances use the same cache store and share the same cached objects. Once an application instance refreshes a cached object, all others use the new object. +* Even if your applications stop and restart, the cached objects are not lost, since they are managed by a separate cache server. + +## How to Use a Distributed Cache Server + +ABP [solution templates](../solution-templates/index.md) come with Redis configured when it is certainly necessary. For example; + +* The [microservice startup template](../solution-templates/microservice/index.md) always comes with [Redis configured](../solution-templates/microservice/distributed-cache.md) and also included as a docker container. + +* The application startup template comes with Redis configured when you select multiple applications, tiered architecture, or some other configuration that requires a distributed cache server. + +In other cases, to keep the dependencies minimal, they come with the default (in-memory) cache configuration. In those cases, if you need a distributed cache server, you should manually switch to a distributed cache provider for your application. + +See the *[Redis Cache](../framework/fundamentals/redis-cache.md)* document if you need to use Redis as the distributed cache server. See [ASP.NET Core's documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) to see how to switch to another cache provider. + +### Installing a Redis Server to Your Local Environment + +If you want to use Redis as your distributed cache provider in your development environment, you can simply use the [official Redis docker image](https://hub.docker.com/_/redis). Once you have [Docker](https://www.docker.com/products/docker-desktop/) in your local machine, you can use the following command to run a Redis container and map the default Redis port: + +````bash +docker run -p 6379:6379 --name RedisServer -d redis +```` + +You can check the [official Redis docker image](https://hub.docker.com/_/redis) document for more options. + +## See Also + +* [ABP Distributed Cache](../framework/fundamentals/caching.md) +* [ASP.NET Core Distributed Cache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) diff --git a/docs/en/release-info/migration-guides/openiddict5-to-6.md b/docs/en/release-info/migration-guides/openiddict5-to-6.md new file mode 100644 index 0000000000..f876f86265 --- /dev/null +++ b/docs/en/release-info/migration-guides/openiddict5-to-6.md @@ -0,0 +1,28 @@ +# OpenIddict 5.x to 6.x Migration Guide + +The 6.0 release of OpenIddict is a major release that introduces breaking changes. + +Check this blog [OpenIddict 6.0 general availability](https://kevinchalet.com/2024/12/17/openiddict-6-0-general-availability/) for the new features introduced in OpenIddict 6.0. and the [Migrate to OpenIddict 6.0](https://documentation.openiddict.com/guides/migration/50-to-60) for more information about the changes. + +In this guide, we will explain the changes you need to make to your ABP application. + +## Constant changes + +The following constants have been renamed: + +| Old Constant Name | New Constant Name | +|---------------------------------------------------------------|-----------------------------------------------------------------| +| `OpenIddictConstants.Permissions.Endpoints.Logout` | `OpenIddictConstants.Permissions.Endpoints.EndSession` | +| `OpenIddictConstants.Permissions.Endpoints.Device` | `OpenIddictConstants.Permissions.Endpoints.DeviceAuthorization` | + + +## IdentityModel packages + +If you have a reference to `IdentityModel` directly, please upgrade the necessary package versions to the latest stable version, which is currently 8.3.0: + +* [System.IdentityModel.Tokens.Jwt](https://www.nuget.org/packages/System.IdentityModel.Tokens.Jwt/) +* [Microsoft.IdentityModel.Protocols.OpenIdConnect](https://www.nuget.org/packages/Microsoft.IdentityModel.Protocols.OpenIdConnect/) +* [Microsoft.IdentityModel.Tokens](https://www.nuget.org/packages/Microsoft.IdentityModel.Tokens/) +* [Microsoft.IdentityModel.JsonWebTokens](https://www.nuget.org/packages/Microsoft.IdentityModel.JsonWebTokens/) + +That's all, it's a simple migration! If you have advanced usage of OpenIddict, please check the [official migration guide](https://documentation.openiddict.com/guides/migration/50-to-60) for more information. diff --git a/docs/en/solution-templates/application-module/images/additional-options.png b/docs/en/solution-templates/application-module/images/additional-options.png new file mode 100644 index 0000000000..216c60e50f Binary files /dev/null and b/docs/en/solution-templates/application-module/images/additional-options.png differ diff --git a/docs/en/solution-templates/application-module/images/create-new-module.png b/docs/en/solution-templates/application-module/images/create-new-module.png new file mode 100644 index 0000000000..0b92070b66 Binary files /dev/null and b/docs/en/solution-templates/application-module/images/create-new-module.png differ diff --git a/docs/en/solution-templates/application-module/images/issuemanagement-module-solution.png b/docs/en/solution-templates/application-module/images/issuemanagement-module-solution.png new file mode 100644 index 0000000000..c0c77fb529 Binary files /dev/null and b/docs/en/solution-templates/application-module/images/issuemanagement-module-solution.png differ diff --git a/docs/en/solution-templates/application-module/images/new-module.png b/docs/en/solution-templates/application-module/images/new-module.png new file mode 100644 index 0000000000..af046d16c3 Binary files /dev/null and b/docs/en/solution-templates/application-module/images/new-module.png differ diff --git a/docs/en/solution-templates/application-module/images/new-solution.png b/docs/en/solution-templates/application-module/images/new-solution.png new file mode 100644 index 0000000000..29e6fe4b74 Binary files /dev/null and b/docs/en/solution-templates/application-module/images/new-solution.png differ diff --git a/docs/en/solution-templates/application-module/images/select-database-provider.png b/docs/en/solution-templates/application-module/images/select-database-provider.png new file mode 100644 index 0000000000..1ff834ee1a Binary files /dev/null and b/docs/en/solution-templates/application-module/images/select-database-provider.png differ diff --git a/docs/en/solution-templates/application-module/images/select-user-interface.png b/docs/en/solution-templates/application-module/images/select-user-interface.png new file mode 100644 index 0000000000..b0eecbc25d Binary files /dev/null and b/docs/en/solution-templates/application-module/images/select-user-interface.png differ diff --git a/docs/en/solution-templates/application-module/images/solution-properties.png b/docs/en/solution-templates/application-module/images/solution-properties.png new file mode 100644 index 0000000000..67549db2c5 Binary files /dev/null and b/docs/en/solution-templates/application-module/images/solution-properties.png differ diff --git a/docs/en/solution-templates/application-module/index.md b/docs/en/solution-templates/application-module/index.md index 857b4ed936..0c0f86ddbd 100644 --- a/docs/en/solution-templates/application-module/index.md +++ b/docs/en/solution-templates/application-module/index.md @@ -4,43 +4,51 @@ This template can be used to create a **reusable [application module](../../modu ## How to Start With? -You can use the [ABP CLI](../../cli) 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. +You can use the [ABP CLI](../../cli) or [ABP Studio](../../studio/overview.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. We will use the ABP Studio for this guide. -First, install the ABP CLI if you haven't installed before: +First, install the ABP Studio if you haven't installed before. You can follow the [installation guide](../../studio/installation.md) for this. -```bash -dotnet tool install -g Volo.Abp.Studio.Cli -``` +### Creating a New Empty Solution -Then use the `abp new` command in an empty folder to create a new solution: +Open the ABP Studio and click the `New solution` button in the welcome page or the `File > New Solution` top menu item. Select the `Empty Solution` template and click the `Next` button. -```bash -abp new-module Acme.BookStore -``` +![New Solution](images/new-solution.png) + +Enter the solution name, select the solution folder and click the `Create` button. + +![Solution Properties](images/solution-properties.png) - `Acme.IssueManagement` is the solution name, like *YourCompany.YourProduct*. You can use single level, two-levels or three-levels naming. -### Specifying the User Interface +> To understand the terms solution, module, and package, refer to the ABP Studio [concepts](../../studio/concepts.md) document. + +### Creating a New DDD Module + +When you create a new solution, the solution explorer on the left side of the screen will appear empty. Right-click on the root of the solution and select `Add > New Module > DDD Module` from the context menu. + +![New Module](images/new-module.png) + +The `Create New Module` dialog will open. Enter the module name and click the `Next` button. + +![Create New Module](images/create-new-module.png) + +Now, you can select the user interface options or leave it empty to create a module without a user interface. A module can support multiple user interfaces, such as MVC, Blazor, Angular, etc., or none at all. Click the `Next` button to specify the database provider. -The template comes without a user interface by default. You can use the `mvc`, `blazor`, `blazor-server`, or `angular` options to include any of these UI layers. You can also combine them. For example, you can use `mvc,angular` to include both MVC and Angular UI. To create a module without a user interface, don't specify any value. +![Select User Interface](images/select-user-interface.png) -````bash -abp new-module Acme.IssueManagement -u mvc,angular -```` +Select the database provider(s) you want to use in your module. You can choose `EntityFrameworkCore`, `MongoDB`, or both. Unlike the user interface options, you must select at least one database provider. Click the `Next` button to see the additional options. -#### Specifying the Database Provider +![Select Database Provider](images/select-database-provider.png) -The template comes with the *EntityFrameworkCore* database provider by default. You can use the `ef` or `mongodb` options to include either of these providers. You can also combine them. For example, you can use `ef,mongodb` to include both EntityFrameworkCore and MongoDB. +You can exclude the test projects from the module by unchecking the `Include Tests` option. Click the `Create` button to create the module. -````bash -abp new-module Acme.IssueManagement -d ef,mongodb -```` +![Additional Options](images/additional-options.png) ## Solution Structure Based on the options you've specified, you will get a slightly different solution structure. If you don't specify any option, you will have a solution like shown below: -![issuemanagement-module-solution](../../images/issuemanagement-module-solution.png) +![issuemanagement-module-solution](images/issuemanagement-module-solution.png) Projects are organized as `src` and`test` folders: diff --git a/docs/en/solution-templates/index.md b/docs/en/solution-templates/index.md index b5bf59688d..739dbc453e 100644 --- a/docs/en/solution-templates/index.md +++ b/docs/en/solution-templates/index.md @@ -1,4 +1,4 @@ -# Solution Templates +# Startup Solution Templates ABP provides pre-architected and production-ready templates to jump start a new solution. diff --git a/docs/en/solution-templates/layered-web-application/authentication.md b/docs/en/solution-templates/layered-web-application/authentication.md new file mode 100644 index 0000000000..0133e95246 --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/authentication.md @@ -0,0 +1,56 @@ +# Layered Solution: Authentication + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Built-In Features", + "Path": "solution-templates/layered-web-application/built-in-features" + }, + "Next": { + "Name": "Database configurations in the Layered solution", + "Path": "solution-templates/layered-web-application/database-configurations" + } +} +``` + +> Some of the features mentioned in this document may not be available in the free version. We're using the **\*** symbol to indicate that a feature is available in the **[Team](https://abp.io/pricing)** and **[Higher](https://abp.io/pricing)** licenses. + +The [Layered solution template](index.md) is fully configured for authentication. All the services and applications are configured to use the [OpenIddict](https://documentation.openiddict.com) library for authentication. They are configured in a common way for authentication. This document explains that common authentication structure. + +If you choose the *Tiered* option while [creating](../../get-started/layered-web-application.md#creating-a-new-solution) the solution, the solution will have the `*.AuthServer` project. + +## OpenIddict + +[OpenIddict](https://documentation.openiddict.com) is an open-source library that provides a simple and easy way to implement an OpenID Connect server in your application. ABP has built-in modules ([OpenIddict](../../modules/openiddict.md), [OpenIddict UI **\***](../../modules/openiddict-pro.md)) to integrate OpenIddict into the solution. + +## Domain Layer + +The layered solution template *Domain* layer is the responsible for the OpenIddict definitions (Applications, Scopes, etc.). Also, it provides the *OpenIddictDataSeedContributor* class to seed the initial data. It creates the default clients (applications) and scopes for the solution. + +The [OpenIddict UI **\***](../../modules/openiddict-pro.md) module is added only if you choose the OpenIddict UI module while creating the solution. + +![new-solution-openiddict-module](images/new-solution-openiddict-module.png) + +The OpenIddict UI **\*** module provides a user interface to manage the OpenIddict entities such as applications, scopes, etc. You can manage these entities from the application UI. + +![openiddict-ui](images/openiddict-ui.png) + +## The Authentication Application + +The solution may include an external authentication server (`auth-server`) application if you select the *Tiered* option during solution creation. Otherwise, the authentication server is integrated into one of the [Web Applications](web-applications.md). + +The authentication server handles token generation, validation, and user account management (e.g., login, registration). It uses the [Account](../../modules/account.md) or [Account Pro **\***](../../modules/account-pro.md) module. The [Account Pro **\***](../../modules/account-pro.md) module additionally supports [social logins](../../modules/account-pro.md#social--external-logins) (e.g., Google, Facebook). Social logins can be enabled, disabled, and configured directly from the application's user interface. + +![account-external-provider](images/account-external-provider.png) + +## Authentication Flows + +Applications in the solution use different authentication flows depending on the application type: + +- **MVC UI Web Application**: + Uses the [Hybrid Flow](https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth) (OpenID Connect Authentication) for user authentication. +- **SPA and Swagger Applications**: + Use the [Authorization Code Flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) to authenticate users. + +Once a user logs into the system and obtains a token from the authentication server, the `*.HttpApi.Host` application use [JWT Bearer Authentication](https://jwt.io/introduction/) to authorize the user's actions. diff --git a/docs/en/solution-templates/layered-web-application/blob-storing.md b/docs/en/solution-templates/layered-web-application/blob-storing.md new file mode 100644 index 0000000000..9e1332e03a --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/blob-storing.md @@ -0,0 +1,51 @@ +# Layered Solution: BLOB Storing + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Multi-Tenancy", + "Path": "solution-templates/layered-web-application/multi-tenancy" + }, + "Next": { + "Name": "CORS Configuration", + "Path": "solution-templates/layered-web-application/cors-configuration" + } +} +``` + +> Some of the features mentioned in this document may not be available in the free version. We're using the **\*** symbol to indicate that a feature is available in the **[Team](https://abp.io/pricing)** and **[Higher](https://abp.io/pricing)** licenses. + +This document explains how to store BLOBs (Binary Large Objects) in a layered solution. It is common to store files, images, videos, and other large objects in a distributed system. You can learn more about BLOB storage in the [BLOB Storing System](../../framework/infrastructure/blob-storing/index.md) documentation. + +In the layered solution template, the [Database Provider](../../framework/infrastructure/blob-storing/database.md) is used to store BLOBs in the database. The `Volo.Abp.BlobStoring.Database.EntityFrameworkCore` or `Volo.Abp.BlobStoring.Database.MongoDB` package provides the necessary implementations to store and retrieve BLOBs in the database. This setup is integrated into the layered solution template and is used in all related projects. You can change the database configuration in the `appsettings.json` file of the related project. + +You can use the `IBlobContainer` or `IBlobContainer` service to store and retrieve BLOBs. Here is an example of storing a BLOB: + +```csharp +public class MyService : ITransientDependency +{ + private readonly IBlobContainer _blobContainer; + + public MyService(IBlobContainer blobContainer) + { + _blobContainer = blobContainer; + } + + public async Task SaveBytesAsync(byte[] bytes) + { + await _blobContainer.SaveAsync("my-blob-1", bytes); + } + + public async Task GetBytesAsync() + { + return await _blobContainer.GetAllBytesOrNullAsync("my-blob-1"); + } +} +``` + +## File Management Module + +The *File Management* module is optional and can be added to the solution during the creation process. It provides a user interface to manage folders and files. You can learn more about the module in the [File Management *](../../modules/file-management.md) document. + +![file-management](images/file-management-index-page.png) diff --git a/docs/en/solution-templates/layered-web-application/built-in-features.md b/docs/en/solution-templates/layered-web-application/built-in-features.md new file mode 100644 index 0000000000..b5dba7c444 --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/built-in-features.md @@ -0,0 +1,25 @@ +# Layered Solution: Built-In Features + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Mobile Applications", + "Path": "solution-templates/layered-web-application/mobile-applications" + }, + "Next": { + "Name": "Authentication", + "Path": "solution-templates/layered-web-application/authentication" + } +} +``` + +The Layered solution template includes several built-in features to help you get started with your layered web application. These features are designed to provide a solid foundation for your application and help you focus on your business logic. This document provides an overview of the built-in features included in the Layered solution template. The following documents explains these features in details: + +* [Authentication](authentication.md) +* [Database configurations](database-configurations.md) +* [Logging (with Serilog)](logging.md) +* [Swagger integration](swagger-integration.md) +* [Multi-Tenancy](multi-tenancy.md) +* [BLOB storing](blob-storing.md) +* [CORS configuration](cors-configuration.md) \ No newline at end of file diff --git a/docs/en/solution-templates/layered-web-application/cors-configuration.md b/docs/en/solution-templates/layered-web-application/cors-configuration.md new file mode 100644 index 0000000000..6df207c107 --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/cors-configuration.md @@ -0,0 +1,37 @@ +# Layered Solution: CORS Configuration + +```json +//[doc-nav] +{ + "Previous": { + "Name": "BLOB Storing", + "Path": "solution-templates/layered-web-application/blob-storing" + }, + "Next": { + "Name": "Helm Charts and Kubernetes", + "Path": "solution-templates/layered-web-application/helm-charts-and-kubernetes" + } +} +``` + +Cross-Origin Resource Sharing (CORS) is a security feature that allows web applications to make requests to a different domain than the one that served the web page. + +In the layered solution template, CORS configuration is applied in the following cases: +- If you select the [Tiered solution](solution-structure.md#tiered-structure-). +- If you choose [Angular](web-applications.md#angular) as the web application type. +- If you choose [Blazor WebAssembly](web-applications.md#blazor-webassembly) as the web application type. +- If you choose [No UI](web-applications.md#no-ui) as the web application type. + +The CORS settings are configured in the `appsettings.json` file of the corresponding project. Typically, the web application serves as the entry point for front-end applications, so it must be configured to accept requests from different origins. + +The default configuration in `appsettings.json` is as follows: + +```json +{ + "App": { + "CorsOrigins": "https://*.MyProjectName.com" + } +} +``` + +You can modify the `CorsOrigins` property to include additional domains or wildcard subdomains as required by your application. \ No newline at end of file diff --git a/docs/en/solution-templates/layered-web-application/database-configurations.md b/docs/en/solution-templates/layered-web-application/database-configurations.md new file mode 100644 index 0000000000..0776062c4f --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/database-configurations.md @@ -0,0 +1,266 @@ +# Layered Solution: Database configurations + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Authentication", + "Path": "solution-templates/layered-web-application/authentication" + }, + "Next": { + "Name": "Logging (with Serilog)", + "Path": "solution-templates/layered-web-application/logging" + } +} +``` + +> Some of the features mentioned in this document may not be available in the free version. We're using the **\*** symbol to indicate that a feature is available in the **[Team](https://abp.io/pricing)** and **[Higher](https://abp.io/pricing)** licenses. + +ABP Studio's Layered Solution Template includes pre-configured database settings. This document explains how to manage database configurations in your solution. + +## Connection Strings + +Connection strings are stored in the `appsettings.json` file. You can customize them for different environments by modifying the respective `appsettings.json` files. The `*.DbMigrator` project and one of the [Web Application](web-applications.md) projects use the `Default` connection string by default. + +To change the connection string for the `Default` key, update the `appsettings.json` file in your project. Connection strings are defined under the `ConnectionStrings` section, as shown below: + +```json +{ + "ConnectionStrings": { + "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=Bookstore;Trusted_Connection=True;TrustServerCertificate=true" + } +} +``` + +### The DbContext Class + +In the `*.EntityFrameworkCore` project, the `DbContext` class is defined. The `DbContext` class is derived from the `AbpDbContext` class, which is a part of the ABP Framework. + +```csharp +[ReplaceDbContext(typeof(IIdentityProDbContext))] +[ReplaceDbContext(typeof(ISaasDbContext))] +[ConnectionStringName("Default")] +public class BookstoreDbContext : + AbpDbContext, + ISaasDbContext, + IIdentityProDbContext +{ + #region Entities from the modules + + // Identity + public DbSet Users { get; set; } + public DbSet Roles { get; set; } + public DbSet ClaimTypes { get; set; } + public DbSet OrganizationUnits { get; set; } + public DbSet SecurityLogs { get; set; } + public DbSet LinkUsers { get; set; } + public DbSet UserDelegations { get; set; } + public DbSet Sessions { get; set; } + + // SaaS + public DbSet Tenants { get; set; } + public DbSet Editions { get; set; } + public DbSet TenantConnectionStrings { get; set; } + + #endregion + + public BookstoreDbContext(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.ConfigurePermissionManagement(); + builder.ConfigureSettingManagement(); + builder.ConfigureBackgroundJobs(); + builder.ConfigureAuditLogging(); + builder.ConfigureFeatureManagement(); + builder.ConfigureIdentityPro(); + builder.ConfigureOpenIddictPro(); + builder.ConfigureLanguageManagement(); + builder.ConfigureSaas(); + builder.ConfigureTextTemplateManagement(); + builder.ConfigureGdpr(); + builder.ConfigureCmsKit(); + builder.ConfigureCmsKitPro(); + builder.ConfigureBlobStoring(); + + /* Configure your own tables/entities inside here */ + + //builder.Entity(b => + //{ + // b.ToTable(BookstoreConsts.DbTablePrefix + "YourEntities", BookstoreConsts.DbSchema); + // b.ConfigureByConvention(); //auto configure for the base class props + // //... + //}); + } +} +``` + +#### ConnectionStringName Attribute + +We're using the *Default* connection string in the `BookstoreDbContext` class. You can change the connection string name by updating the `ConnectionStringName` attribute. + +```csharp +[ConnectionStringName("Default")] +``` + +[The `ConnectionStringName` attribute](../../framework/fundamentals/connection-strings.md#set-the-connection-string-name) defines the unique name of the connection string that is being used by that `DbContext` class. It matches with the connection string defined in the `appsettings.json` file. That name is also used in database migrations to distinguish different database schemas, and used as the key while storing tenant connection strings for a multi-tenant system. + +#### ReplaceDbContext Attribute + +```csharp +[ReplaceDbContext(typeof(IIdentityProDbContext))] +[ReplaceDbContext(typeof(ISaasDbContext))] +``` + +The application DbContext utilizes the [Identity](../../modules/identity.md) and [Saas **\***](../../modules/saas.md) modules and creates a single database that contains these modules database schemas. These modules define their own `DbContext` class normally. But [the `ReplaceDbContext` attribute](../../framework/data/entity-framework-core/index.md#replace-other-dbcontextes) tells to ABP to use this (`BookstoreDbContext`) `DbContext` class instead of the `DbContext` classes defined by these modules. Technically, it replaces the given `DbContext` classes on runtime. We are doing that to ensure that we have a single (merged) database schema, single database migration path and a single database transaction operation when we work these multiple modules. When we replace a `DbContext`, we should implement its interface as done with the `BookstoreDbContext` class: + +````csharp +public class BookstoreDbContext : + AbpDbContext, + ISaasDbContext, + IIdentityProDbContext +```` + +* That class implements `ISaasDbContext` and `IIdentityProDbContext`, so these modules can use it. + +As the next part, the `BookstoreDbContext` class defines the following properties those are forced by the implemented interfaces: + +```csharp +// Identity +public DbSet Users { get; set; } +public DbSet Roles { get; set; } +public DbSet ClaimTypes { get; set; } +public DbSet OrganizationUnits { get; set; } +public DbSet SecurityLogs { get; set; } +public DbSet LinkUsers { get; set; } +public DbSet UserDelegations { get; set; } +public DbSet Sessions { get; set; } + +// SaaS +public DbSet Tenants { get; set; } +public DbSet Editions { get; set; } +public DbSet TenantConnectionStrings { get; set; } +``` + +#### OnModelCreating Method + +The `OnModelCreating` method is used to configure the database schema. It calls the `Configure*` methods of the ABP Framework to configure the database schema for the modules. You can also configure your own tables/entities inside this method. + +```csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + base.OnModelCreating(builder); + + builder.ConfigurePermissionManagement(); + builder.ConfigureSettingManagement(); + builder.ConfigureBackgroundJobs(); + builder.ConfigureAuditLogging(); + builder.ConfigureFeatureManagement(); + builder.ConfigureIdentityPro(); + builder.ConfigureOpenIddictPro(); + builder.ConfigureLanguageManagement(); + builder.ConfigureSaas(); + builder.ConfigureTextTemplateManagement(); + builder.ConfigureGdpr(); + builder.ConfigureCmsKit(); + builder.ConfigureCmsKitPro(); + builder.ConfigureBlobStoring(); + + /* Configure your own tables/entities inside here */ + + //builder.Entity(b => + //{ + // b.ToTable(BookstoreConsts.DbTablePrefix + "YourEntities", BookstoreConsts.DbSchema); + // b.ConfigureByConvention(); //auto configure for the base class props + // //... + //}); +} +``` + +> The `Configure*` methods are extension methods defined in each module's `EntityFrameworkCore` project. These methods are used to configure the database schema for their respective modules. At runtime, the `DbContext` class is replaced by the `BookstoreDbContext` class only for the `DbContext` classes that use the `ReplaceDbContext` attribute. For other modules, their own dedicated `DbContext` classes are used without replacement. + +### The `IDesignTimeDbContextFactory` Implementation + +The `IDesignTimeDbContextFactory` interface is used to create a `DbContext` instance at design time. It is used by EF Core tools to create migrations and update the database. The `BookstoreDbContextFactory` class implements the `IDesignTimeDbContextFactory` interface to create a `BookstoreMigrationsDbContext` instance. + +```csharp +public class BookstoreDbContextFactory : IDesignTimeDbContextFactory +{ + public BookstoreDbContext CreateDbContext(string[] args) + { + var configuration = BuildConfiguration(); + + BookstoreEfCoreEntityExtensionMappings.Configure(); + + var builder = new DbContextOptionsBuilder() + .UseSqlServer(configuration.GetConnectionString("Default")); + + return new BookstoreDbContext(builder.Options); + } + + private static IConfigurationRoot BuildConfiguration() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "../Acme.Bookstore.DbMigrator/")) + .AddJsonFile("appsettings.json", optional: false); + + return builder.Build(); + } +} +``` + +### Configuration + +In the `*.EntityFrameworkCore` project, the `BookstoreEntityFrameworkCoreModule` class is used to configure the database context. + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddAbpDbContext(options => + { + /* Remove "includeAllEntities: true" to create + * default repositories only for aggregate roots */ + options.AddDefaultRepositories(includeAllEntities: true); + }); + + Configure(options => + { + /* The main point to change your DBMS. + * See also BookstoreDbContextFactory for EF Core tooling. */ + options.UseSqlServer(); + }); + +} +``` + +We are basically setting the SQL Server as the default DBMS for this application. and registering the `BookstoreDbContext` class to the [dependency injection](../../framework/fundamentals/dependency-injection.md) system. + +### SaaS Module: The Tenant Management UI **\*** + +SaaS module provides the necessary UI to set and change connection string for tenants and trigger the database migrations. + +#### The Connection String Management Modal + +You can click to the *Database Connection Strings* command in the *Actions* dropdown button for a tenant in the *Tenants* page of the SaaS module: + +![Database Connection Strings](images/database-connection-strings.png) + +It opens the *Database Connection Strings* modal as shown below: + +![Database Connection Strings Modal](images/database-connection-strings-modal.png) + +Here, we can set a *Default connection string* for the tenant. + +When you make the changes and save the dialog, the database is automatically created and migrated. If you later update the connection string (for example if you change the database name), it will also trigger the database migration process again. + +#### Manually Applying the Database Migrations + +If you need to manually trigger the database migrations for a specific tenant, click the *Actions* dropdown for the related tenant and select the *Apply Database Migrations* command on the *Tenant Management* page of the SaaS module: + +![Apply Database Migrations](images/apply-database-migrations.png) diff --git a/docs/en/solution-templates/layered-web-application/db-migrator.md b/docs/en/solution-templates/layered-web-application/db-migrator.md new file mode 100644 index 0000000000..50fd38d836 --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/db-migrator.md @@ -0,0 +1,41 @@ +# Layered Solution: Db Migrator + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Web Applications", + "Path": "solution-templates/layered-web-application/web-applications" + }, + "Next": { + "Name": "Mobile Applications", + "Path": "solution-templates/layered-web-application/mobile-applications" + } +} +```` + +## Db Migrator Project + +The Db Migrator project is a console application designed to handle database schema migrations and seed data population. It operates as a standalone application that can be executed on-demand or integrated into a CI/CD pipeline. + +### Usage + +You can run the Db Migrator application: +- From the command line. +- Directly from Visual Studio. + +### Configuration + +The Db Migrator project maintains its own configuration, separate from the main application. If you need to update the database connection string or any related settings, ensure that changes are applied consistently to both the main application and the Db Migrator to avoid discrepancies. + +## Folder Structure + +In the `*.DbMigrator` project, you will find the `DbMigratorHostedService` class, which is responsible for executing database migrations and seeding data. This class is registered in the `Program` class and starts running when the application is launched. + +### Layers and Responsibilities + +- **`*.Domain` Layer**: + Contains the `Data` folder, which holds the necessary classes for managing database migrations and seed data. However, since the `*.Domain` layer does not reference the `EntityFrameworkCore` package, it only defines the abstraction for data migration. + +- **`*.EntityFrameworkCore` Layer**: + This layer is responsible for implementing database schema migrations. It includes the `EntityFrameworkCore[ProjectName]DbSchemaMigrator` class, which handles the actual migration logic using the `EntityFrameworkCore` package. diff --git a/docs/en/solution-templates/layered-web-application/helm-charts-and-kubernetes.md b/docs/en/solution-templates/layered-web-application/helm-charts-and-kubernetes.md new file mode 100644 index 0000000000..dcdd805e7f --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/helm-charts-and-kubernetes.md @@ -0,0 +1,65 @@ +# Layered Solution: Helm Charts and Kubernetes + +````json +//[doc-nav] +{ + "Previous": { + "Name": "CORS configuration", + "Path": "solution-templates/layered-web-application/cors-configuration" + } +} +```` + +> You must have an ABP Business or a higher license to be able to use the Kubernetes features. + +This document explains how to deploy the layered solution to a Kubernetes cluster using [Helm](https://helm.sh/) charts. The layered solution template includes Helm charts for each application and infrastructure (Redis, RabbitMQ, etc). You can use these charts to deploy the solution to a Kubernetes cluster. You can see the Helm charts in the `etc/helm` folder of the solution. + +## Folder Structure + +The folder structure of the Helm charts is as follows: + +![helm-folder](images/helm-folder.png) + +> You might have different charts based on the solution template options you selected while creating the solution. + +* **bookstore**: The Helm chart for the `Bookstore` solution. The folder name should be the same as your project name. + * **charts**: The sub-charts of the solution. Each application and infrastructure has its own chart. + * **templates**: The templates of the solution. It includes the ingress host URLs. + * **Chart.yaml**: The chart metadata. + * **values.bookstore-local.yaml**: The override values file for the [Kubernetes profile](../../studio/kubernetes.md#profile). It should follow the naming convention for your project name. + * **values.yaml**: The default values file for the chart. +* **build-all-images.ps1**: A PowerShell script to build all Docker images of the solution. +* **build-image.ps1**: A PowerShell script to build a Docker image of a specified project. +* **create-tls-secrets.ps1**: A PowerShell script to create local TLS secrets for the ingress controller. It's important when you try to [intercept a service](../../studio/kubernetes.md#intercept-a-service) and run it locally. +* **install.ps1**: A PowerShell script to install the solution to a Kubernetes cluster. You can override the default argument values. +* **uninstall.ps1**: A PowerShell script to uninstall the solution from a Kubernetes cluster. You can override the default argument values. + +## Installing the Helm Charts + +You can install the solution to a Kubernetes cluster using the `install.ps1` script. The script has the following arguments: + +* **ChartName**: Default value is the project name. You can create different charts and specify the chart name. In ABP Studio [Kubernetes Main Chart](../../studio/kubernetes.md#main-chart) *Install Chart(s)* command automatically sets the chart name. +* **Namespace**: The namespace to install the Kubernetes resources. Default value is the project name with the `-local` suffix. +* **ReleaseName**: The release name of the Helm chart. Default value is the project name with the `-local` suffix. +* **DotnetEnvironment**: The environment to run the application. Default value is `Staging`. +* **User**: The user responsible for installing the Kubernetes resources. The application will automatically set the user name if you configure it under [Specify the User](../../studio/kubernetes.md#specify-the-user). + +Before running the script, you need to build the Docker images of the solution. You can use the `build-all-images.ps1` script to build all Docker images of the solution. Afterwards, make sure that you have a Kubernetes TLS secret for the ingress controller. It is automatically created when you create the solution; however, if you clone the solution from a repository, you need to create it manually. You can use the `create-tls-secrets.ps1` script to create the TLS secret. Then you can run the `install.ps1` script to install the solution to a Kubernetes cluster. + +## Uninstalling the Helm Charts + +You can uninstall the solution from a Kubernetes cluster using the `uninstall.ps1` script. The script has the following arguments: + +* **Namespace**: The namespace to uninstall the helm chart. Default value is the project name with the `-local` suffix. +* **ReleaseName**: The release name of the Helm chart. Default value is the project name with the `-local` suffix. +* **User**: The user responsible for uninstalling the Kubernetes resources. The application will automatically set the user name if you configure it under [Specify the User](../../studio/kubernetes.md#specify-the-user). + +You can run the `uninstall.ps1` script to uninstall the solution from a Kubernetes cluster. + +```bash +./uninstall.ps1 +``` + +Additionally, in ABP Studio [Kubernetes](../../studio/kubernetes.md) feature, you can do the same operations more easily. You can use the Install Chart(s) and Uninstall Chart(s) commands to install and uninstall the solution to a Kubernetes cluster. Also, use the Build Docker Image(s) command to build the Docker images of the solution. + +![kubernetes](images/kubernetes.png) \ No newline at end of file diff --git a/docs/en/solution-templates/layered-web-application/images/account-external-provider.png b/docs/en/solution-templates/layered-web-application/images/account-external-provider.png new file mode 100644 index 0000000000..acfefeee7c Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/account-external-provider.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/angular-folder-structure.png b/docs/en/solution-templates/layered-web-application/images/angular-folder-structure.png new file mode 100644 index 0000000000..95bfc8986f Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/angular-folder-structure.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/angular-template-structure-diagram.png b/docs/en/solution-templates/layered-web-application/images/angular-template-structure-diagram.png new file mode 100644 index 0000000000..dd7a4e5cc7 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/angular-template-structure-diagram.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/apply-database-migrations.png b/docs/en/solution-templates/layered-web-application/images/apply-database-migrations.png new file mode 100644 index 0000000000..28d5f73786 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/apply-database-migrations.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/bookstore-solution-tiered.png b/docs/en/solution-templates/layered-web-application/images/bookstore-solution-tiered.png new file mode 100644 index 0000000000..06d21a2028 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/bookstore-solution-tiered.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/database-connection-strings-modal.png b/docs/en/solution-templates/layered-web-application/images/database-connection-strings-modal.png new file mode 100644 index 0000000000..7dc2fd7243 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/database-connection-strings-modal.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/database-connection-strings.png b/docs/en/solution-templates/layered-web-application/images/database-connection-strings.png new file mode 100644 index 0000000000..4d8b8f4e43 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/database-connection-strings.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/file-management-index-page.png b/docs/en/solution-templates/layered-web-application/images/file-management-index-page.png new file mode 100644 index 0000000000..c19d41f006 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/file-management-index-page.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/helm-folder.png b/docs/en/solution-templates/layered-web-application/images/helm-folder.png new file mode 100644 index 0000000000..ee5c055a36 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/helm-folder.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/kubernetes.png b/docs/en/solution-templates/layered-web-application/images/kubernetes.png new file mode 100644 index 0000000000..a32b81f5d1 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/kubernetes.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/layered-project-dependencies.png b/docs/en/solution-templates/layered-web-application/images/layered-project-dependencies.png new file mode 100644 index 0000000000..1d5c4f1195 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/layered-project-dependencies.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/layered-solution-in-explorer.png b/docs/en/solution-templates/layered-web-application/images/layered-solution-in-explorer.png new file mode 100644 index 0000000000..1a83a497bb Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/layered-solution-in-explorer.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/mobile-applications.png b/docs/en/solution-templates/layered-web-application/images/mobile-applications.png new file mode 100644 index 0000000000..873d2d9705 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/mobile-applications.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/new-solution-openiddict-module.png b/docs/en/solution-templates/layered-web-application/images/new-solution-openiddict-module.png new file mode 100644 index 0000000000..a3cce0c7bc Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/new-solution-openiddict-module.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/open-solution-with-explorer.png b/docs/en/solution-templates/layered-web-application/images/open-solution-with-explorer.png new file mode 100644 index 0000000000..368b607ad2 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/open-solution-with-explorer.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/openiddict-ui.png b/docs/en/solution-templates/layered-web-application/images/openiddict-ui.png new file mode 100644 index 0000000000..449a3efcfd Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/openiddict-ui.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/run-solution.png b/docs/en/solution-templates/layered-web-application/images/run-solution.png new file mode 100644 index 0000000000..deab922ba7 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/run-solution.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/saas-module-selection.png b/docs/en/solution-templates/layered-web-application/images/saas-module-selection.png new file mode 100644 index 0000000000..74b9857bd7 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/saas-module-selection.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/solution-folders.png b/docs/en/solution-templates/layered-web-application/images/solution-folders.png new file mode 100644 index 0000000000..b09c115145 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/solution-folders.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/solutionrunner-maui-targetframework.png b/docs/en/solution-templates/layered-web-application/images/solutionrunner-maui-targetframework.png new file mode 100644 index 0000000000..cd9b92c1f2 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/solutionrunner-maui-targetframework.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/tiered-solution-applications-authserver.png b/docs/en/solution-templates/layered-web-application/images/tiered-solution-applications-authserver.png new file mode 100644 index 0000000000..1b79af73d0 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/tiered-solution-applications-authserver.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/tiered-solution-servers.png b/docs/en/solution-templates/layered-web-application/images/tiered-solution-servers.png new file mode 100644 index 0000000000..77233195d3 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/tiered-solution-servers.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/web-applications.png b/docs/en/solution-templates/layered-web-application/images/web-applications.png new file mode 100644 index 0000000000..d232b37446 Binary files /dev/null and b/docs/en/solution-templates/layered-web-application/images/web-applications.png differ diff --git a/docs/en/solution-templates/layered-web-application/index.md b/docs/en/solution-templates/layered-web-application/index.md index 5e0d64b2d8..d579b317b0 100644 --- a/docs/en/solution-templates/layered-web-application/index.md +++ b/docs/en/solution-templates/layered-web-application/index.md @@ -1,465 +1,35 @@ -# Layered Application Solution Template +# ABP Studio: Layered Solution Template -This template provides a layered application structure based on the [Domain Driven Design](../../framework/architecture/domain-driven-design) (DDD) practices. - -## Getting Started - -This document explains **the solution structure** and projects in details. If you want to start quickly, follow the guides below: - -* [The getting started document](../../get-started/layered-web-application.md) explains how to create a new application in a few minutes. -* [The application development tutorial](../../tutorials/book-store/part-01.md) explains step by step application development. - -## How to Start With? - -You can use the [ABP CLI](../../cli) 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: - -````bash -dotnet tool install -g Volo.Abp.Studio.Cli -```` - -Then use the `abp new` command in an empty folder to create a new solution: - -````bash -abp new Acme.BookStore -t app -```` - -* `Acme.BookStore` is the solution name, like *YourCompany.YourProduct*. You can use single-level, two-level or three-level naming. -* This example specified the template name (`-t` or `--template` option). However, `app` is already the default template if you didn't specify it. - -### Specify the UI Framework - -This template provides multiple UI frameworks: - -* `mvc`: ASP.NET Core MVC UI with Razor Pages (default) -* `blazor`: Blazor UI -* `blazor-server`: Blazor Server UI -* `angular`: Angular UI - -Use the `-u` or `--ui` option to specify the UI framework: - -````bash -abp new Acme.BookStore -u angular -```` - -### Specify the Database Provider - -This template supports the following database providers: - -- `ef`: Entity Framework Core (default) -- `mongodb`: MongoDB - -Use `-d` (or `--database-provider`) option to specify the database provider: - -````bash -abp new Acme.BookStore -d mongodb -```` - -### Specify the Mobile Application Framework - -This template supports the following mobile application frameworks: - -- `react-native`: React Native (*Available for* ***Team*** *or higher licenses*) - -Use the `-m` (or `--mobile`) option to specify the mobile application framework: - -````bash -abp new Acme.BookStore -m react-native -```` - -* [The getting started document](../../get-started/layered-web-application.md) explains how to create a new application with this startup template. -* [The application development tutorial](../../tutorials/book-store/part-01.md) explains step by step application development with this startup template. - -## Solution Structure - -Based on the options you've specified, you will get a slightly different solution structure. - -### Default Structure - -If you don't specify any additional options, you will have a solution as shown below: - -![bookstore-rider-solution-v6](../../images/solution-structure-solution-explorer-rider.png) - -Projects are organized in `src` and `test` folders. `src` folder contains the actual application which is layered based on [DDD](../../framework/architecture/domain-driven-design) principles as mentioned before. - -The diagram below shows the layers & project dependencies of the application: - -![layered-project-dependencies](../../images/layered-project-dependencies.png) - -Each section below will explain the related project & its dependencies. - -#### .Domain.Shared Project - -This project contains constants, enums and other objects these are actually a part of the domain layer, but needed to be used by all layers/projects in the solution. - -A `BookType` enum and a `BookConsts` class (which may have some constant fields for the `Book` entity, like `MaxNameLength`) are good candidates for this project. - -* This project has no dependency on other projects in the solution. All other projects depend on this one directly or indirectly. - -#### .Domain Project - -This is the domain layer of the solution. It mainly contains [entities, aggregate roots](../../framework/architecture/domain-driven-design/entities.md), [domain services](../../framework/architecture/domain-driven-design/domain-services.md), [value objects](../../framework/architecture/domain-driven-design/value-objects.md), [repository interfaces](../../framework/architecture/domain-driven-design/repositories.md) and other domain objects. - -A `Book` entity, a `BookManager` domain service and an `IBookRepository` interface are good candidates for this project. - -* Depends on the `.Domain.Shared` because it uses constants, enums and other objects defined in that project. - -#### .Application.Contracts Project - -This project mainly contains [application service](../../framework/architecture/domain-driven-design/application-services.md) **interfaces** and [Data Transfer Objects](../../framework/architecture/domain-driven-design/data-transfer-objects.md) (DTO) of the application layer. It exists to separate the interface & implementation of the application layer. In this way, the interface project can be shared to the clients as a contract package. - -An `IBookAppService` interface and a `BookCreationDto` class are good candidates for this project. - -* Depends on the `.Domain.Shared` because it may use constants, enums and other shared objects of this project in the application service interfaces and DTOs. - -#### .Application Project - -This project contains the [application service](../../framework/architecture/domain-driven-design/application-services.md) **implementations** of the interfaces defined in the `.Application.Contracts` project. - -A `BookAppService` class is a good candidate for this project. - -* Depends on the `.Application.Contracts` project to be able to implement the interfaces and use the DTOs. -* Depends on the `.Domain` project to be able to use domain objects (entities, repository interfaces... etc.) to perform the application logic. - -#### .EntityFrameworkCore Project - -This is the integration project for the EF Core. It defines the `DbContext` and implements repository interfaces defined in the `.Domain` project. - -* Depends on the `.Domain` project to be able to reference to entities and repository interfaces. - -> This project is available only if you are using EF Core as the database provider. If you select another database provider, its name will be different. - -#### .DbMigrator Project - -This is a console application that simplifies the execution of database migrations on development and production environments. When you run this application, it: - -* Creates the database if necessary. -* Applies the pending database migrations. -* Seeds initial data if needed. - -> This project has its own `appsettings.json` file. So, if you want to change the database connection string, remember to change this file too. - -Especially, seeding initial data is important at this point. ABP has a modular data seed infrastructure. See [its documentation](../../framework/infrastructure/data-seeding.md) for more about the data seeding. - -While creating database & applying migrations seem only necessary for relational databases, this project comes even if you choose a NoSQL database provider (like MongoDB). In that case, it still seeds the initial data which is necessary for the application. - -* Depends on the `.EntityFrameworkCore` project (for EF Core) since it needs to access to the migrations. -* Depends on the `.Application.Contracts` project to be able to access permission definitions, because the initial data seeder grants all permissions to the admin role by default. - -#### .HttpApi Project - -This project is used to define your API Controllers. - -Most of the time you don't need to manually define API Controllers since ABP's [Auto API Controllers](../../framework/api-development/auto-controllers.md) feature creates them automagically based on your application layer. However, in case of you need to write API controllers, this is the best place to do it. - -* Depends on the `.Application.Contracts` project to be able to inject the application service interfaces. - -#### .HttpApi.Client Project - -This is a project that defines C# client proxies to use the HTTP APIs of the solution. You can share this library to 3rd-party clients, so they can easily consume your HTTP APIs in their Dotnet applications (For other types of applications, they can still use your APIs, either manually or using a tool in their own platform) - -Most of the time you don't need to manually create C# client proxies, thanks to ABP's [Dynamic C# API Clients](../../framework/api-development/dynamic-csharp-clients.md) feature. - -`.HttpApi.Client.ConsoleTestApp` project is a console application created to demonstrate the usage of the client proxies. - -* Depends on the `.Application.Contracts` project to be able to share the same application service interfaces and DTOs with the remote service. - -> You can delete this project & dependencies if you don't need to create C# client proxies for your APIs. - -#### .Web Project - -This project contains the User Interface (UI) of the application if you are using ASP.NET Core MVC UI. It contains Razor pages, JavaScript files, CSS files, images and so on... - -This project contains the main `appsettings.json` file that contains the connection string and other configurations of the application. - -* Depends on the `.HttpApi` project since the UI layer needs to use APIs and the application service interfaces of the solution. - -> If you check the source code of the `.Web.csproj` file, you will see the references to the `.Application` and the `.EntityFrameworkCore` projects. -> -> These references are actually not needed while coding your UI layer, because the UI layer normally doesn't depend on the EF Core or the Application layer's implementation. These startup templates are ready for tiered deployment, where the API layer is hosted on a separate server than the UI layer. -> -> However, if you don't choose the `--tiered` option, these references will be in the .Web project to be able to host the Web, API and application layers in a single application endpoint. -> -> This gives you the ability to use domain entities & repositories in your presentation layer. However, this is considered as a bad practice according to DDD. - -#### Test Projects - -The solution has multiple test projects, one for each layer: - -* `.Domain.Tests` is used to test the domain layer. -* `.Application.Tests` is used to test the application layer. -* `.EntityFrameworkCore.Tests` is used to test EF Core configuration and custom repositories. -* `.Web.Tests` is used to test the UI (if you are using ASP.NET Core MVC UI). -* `.TestBase` is a base (shared) project for all tests. - -In addition, `.HttpApi.Client.ConsoleTestApp` is a console application (not an automated test project) which demonstrate the usage of HTTP APIs from a .NET application. - -Test projects are prepared for integration testing; - -* It is fully integrated into the ABP and all services in your application. -* It uses SQLite in-memory database for EF Core. For MongoDB, it uses the [EphemeralMongo](https://github.com/asimmon/ephemeral-mongo) library. -* Authorization is disabled, so any application service can be easily used in tests. - -You can still create unit tests for your classes which will be harder to write (because you will need to prepare mock/fake objects), but faster to run (because it only tests a single class and skips all the initialization processes). - -#### How to Run? - -Set `.Web` as the startup project and run the application. The default username is `admin` and the password is `1q2w3E*`. - -See [Getting Started With the ASP.NET Core MVC Template](../../get-started/layered-web-application.md) for more information. - -### Tiered Structure - -If you have selected the ASP.NET Core UI and specified the `--tiered` option, the solution created will be a tiered solution. The purpose of the tiered structure is to be able to **deploy Web applications and HTTP API to different servers**: - -![bookstore-visual-studio-solution-v3](../../images/tiered-solution-servers.png) - -* Browser runs your UI by executing HTML, CSS & JavaScript. -* Web servers host static UI files (CSS, JavaScript, image... etc.) & dynamic components (e.g. Razor pages). It performs HTTP requests to the API server to execute the business logic of the application. -* The API Server hosts the HTTP APIs which then use the application & domain layers of the application to perform the business logic. -* Finally, database server hosts your database. - -So, the resulting solution allows a 4-tiered deployment, by comparing to 3-tiered deployment of the default structure explained before. - -> Unless you actually need such a 4-tiered deployment, it's suggested to go with the default structure which is simpler to develop, deploy and maintain. - -The solution structure is shown below: - -![bookstore-rider-solution-v6](../../images/bookstore-rider-solution-tiered.png) - -As different from the default structure, two new projects come into play: `.AuthServer` & `.HttpApi.Host`. - -#### .AuthServer Project - -This project is used as an authentication server for other projects. `.Web` project uses OpenId Connect Authentication to get identity and access tokens for the current user from the AuthServer. Then uses the access token to call the HTTP API server. HTTP API server uses bearer token authentication to obtain claims from the access token to authorize the current user. - -![tiered-solution-applications](../../images/tiered-solution-applications-authserver.png) - -ABP uses the [OpenIddict Module](../../modules/openiddict.md) that uses the open-source [OpenIddict-core](https://github.com/openiddict/openiddict-core) library for the authentication between applications. See [OpenIddict documentation](https://documentation.openiddict.com/) for details about the OpenIddict and OpenID Connect protocol. - -It has its own `appsettings.json` that contains database connection and other configurations. - -#### .HttpApi.Host Project - -This project is an application that hosts the API of the solution. It has its own `appsettings.json` that contains database connection and other configurations. - -#### .Web Project - -Just like the default structure, this project contains the User Interface (UI) of the application. It contains razor pages, JavaScript files, style files, images and so on... - -This project contains an `appsettings.json` file, but this time it does not have a connection string because it never connects to the database. Instead, it mainly contains the endpoint of the remote API server and the authentication server. - -#### Pre-requirements - -* [Redis](https://redis.io/): The applications use Redis as a distributed cache. So, you need to have Redis installed & running. - -#### How to Run? - -You should run the application with the given order: - -* First, run the `.AuthServer` since other applications depend on it. -* Then run the `.HttpApi.Host` since it is used by the `.Web` application. -* Finally, you can run the `.Web` project and login to the application (using `admin` as the username and `1q2w3E*` as the password). - -### Blazor UI -If you choose `Blazor` as the UI Framework (using the `-u blazor` or `-u blazor-server` option), the solution will have a project named `.Blazor`. This project contains the Blazor UI application. According to your choice, it will be a Blazor WebAssembly or Blazor Server application. If Blazor WebAssembly is selected, the solution will also have a `.HttpApi.Host`. This project is an ASP.NET Core application that hosts the backend application for the Blazor single page application. - -#### .Blazor Project (Server) -The Blazor Server project is similar to the ASP.NET Core MVC project. It replaces `.Web` project with `.Blazor` in the solution structure above. It has the same folder structure and the same application flow. Since it's an ASP.NET Core application, it can contain **.cshtml** files and **.razor** components at the same time. If routing matches a razor component, the Blazor UI will be used. Otherwise, the request will be handled by the MVC framework. - -![abp solution structure blazor server](../../images/layered-project-dependencies-blazor-server.png) - -#### .Blazor Project (WebAssembly) -The Blazor WebAssembly project is a single page application that runs on the browser. You'll see it as `.Blazor` project in the solution. It uses the `.HttpApi.Host` project to communicate with the backend. It can't be used without the backend application. It contains only **.razor** components. It's a pure client-side application. It doesn't have any server-side code. Everything in this layer will be for the client side. - -![abp solution structure blazor wasm](../../images/layered-project-dependencies-blazor-wasm.png) - -### Angular UI - -If you choose `Angular` as the UI framework (using the `-u angular` option), the solution is being separated into two folders: - -* `angular` folder contains the Angular UI application, the client-side code. -* `aspnet-core` folder contains the ASP.NET Core solution, the server-side code. - -The server-side is similar to the solution described above. `*.HttpApi.Host` project serves the API, so the `Angular` application consumes it. - -Angular application folder structure looks like below: - -![angular-folder-structure](../../images/angular-folder-structure.png) - - -Each of ABP modules is an NPM package. Some ABP modules are added as a dependency in `package.json`. These modules install with their dependencies. To see all ABP packages, you can run the following command in the `angular` folder: - -```bash -yarn list --pattern abp -``` - -Angular application module structure: - -![Angular template structure diagram](../../images/angular-template-structure-diagram.png) - -#### AppModule - -`AppModule` is the root module of the application. Some of the ABP modules and some essential modules are imported to `AppModule`. - -ABP Config modules have also been imported to `AppModule`ย for initial requirements of the lazy-loadable ABP modules. - -#### AppRoutingModule - -There are lazy-loadable ABP modules in the `AppRoutingModule` as routes. - -> Paths of ABP Modules should not be changed. - -You should add `routes` property in the `data` object to add a link on the menu to redirect to your custom pages. - -```js +````json +//[doc-nav] { - path: 'dashboard', - loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), - canActivate: [authGuard, permissionGuard], - data: { - routes: { - name: 'ProjectName::Menu:Dashboard', - order: 2, - iconClass: 'fa fa-dashboard', - requiredPolicy: 'ProjectName.Dashboard.Host' - } as ABP.Route - } + "Next": { + "Name": "Overview", + "Path": "solution-templates/layered-web-application/overview" + } } -``` -In the above example; -* If the user is not logged in, authGuard blocks access and redirects to the login page. -* permissionGuard checks the user's permission with the `requiredPolicy` property of the `routes` object. If the user is not authorized to access the page, the 403 page appears. -* The `name` property of `routes` is the menu link label. A localization key can be defined. -* The `iconClass` property of the `routes` object is the menu link icon class. -* The `requiredPolicy` property of the `routes` object is the required policy key to access the page. - -After the above `routes` definition, if the user is authorized, the dashboard link will appear on the menu. - -#### Shared Module - -The modules that may be required for all modules have been imported to the `SharedModule`. You should import `SharedModule` to all modules. - -See the [Sharing Modules](https://angular.io/guide/sharing-ngmodules) document. - -#### Environments - -The files under the `src/environments` folder have the essential configuration of the application. - -#### Home Module - -Home module is an example lazy-loadable module that loads on the root address of the application. - -#### Styles - -The required style files are added to the `styles` array in `angular.json`. `AppComponent` loads some style files lazily via `LazyLoadService` after the main bundle is loaded to shorten the first rendering time. - -#### Testing - -You should create your tests in the same folder as the file you want to test. - -See the [testing document](https://angular.io/guide/testing). - -#### Depended Packages - -* [NG Bootstrap](https://ng-bootstrap.github.io/) is used as UI component library. -* [NGXS](https://www.ngxs.io/) is used as state management library. -* [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) is used to support for OAuth 2 and OpenId Connect (OIDC). -* [Chart.js](https://www.chartjs.org/) is used to create widgets. -* [ngx-validate](https://github.com/ng-turkey/ngx-validate) is used for dynamic validation of reactive forms. - -### React Native - -If the `-m react-native` option is specified in the new project command, the solution includes the [React Native](https://reactnative.dev/) application in the `react-native` folder. - -The server-side is similar to the solution described above. `*.HttpApi.Host` project serves the API, so the React Native application consumes it. - -The React Native application was generated with [Expo](https://expo.io/). Expo is a set of tools built around React Native to help you quickly start an app and, while it has many features. - -React Native application folder structure as like below: - -![react-native-folder-structure](../../images/react-native-folder-structure.png) - -* `App.js` is the bootstrap component of the application. -* `Environment.js` file has the essential configuration of the application. `prod` and `dev` configurations are defined in this file. -* [Contexts](https://reactjs.org/docs/context.html) are created in the `src/contexts` folder. -* [Higher order components](https://reactjs.org/docs/higher-order-components.html) are created in the `src/hocs` folder. -* [Custom hooks](https://reactjs.org/docs/hooks-custom.html#extracting-a-custom-hook) are created in `src/hooks`. -* [Axios interceptors](https://github.com/axios/axios#interceptors) are created in the `src/interceptors` folder. -* Utility functions are exported from `src/utils` folder. - -#### Components - -Components that can be used on all screens are created in the `src/components` folder. All components have been created as a function that is able to use [hooks](https://reactjs.org/docs/hooks-intro.html). - -#### Screens - -![react-native-navigation-structure](../../images/react-native-navigation-structure.png) - -Screens are created by creating folders that separate their names in the `src/screens` folder. Certain parts of some screens can be split into components. - -Each screen is used in a navigator in the `src/navigators` folder. - -#### Navigation - -[React Navigation](https://reactnavigation.org/) is used as a navigation library. Navigators are created in the `src/navigators`. A [drawer](https://reactnavigation.org/docs/drawer-based-navigation/) navigator and several [stack](https://reactnavigation.org/docs/hello-react-navigation/#installing-the-stack-navigator-library) navigators have been created in this folder. See the [above diagram](#screens) for the navigation structure. - -#### State Management - -[Redux](https://redux.js.org/) is used as a state management library. [Redux Toolkit](https://redux-toolkit.js.org/) library is used as a toolset for efficient Redux development. - -Actions, reducers, sagas and selectors are created in the `src/store` folder. Store folder is as below: - -![react-native-store-folder](../../images/react-native-store-folder.png) - -* [**Store**](https://redux.js.org/basics/store) is defined in the `src/store/index.js` file. -* [**Actions**](https://redux.js.org/basics/actions/) are payloads of information that send data from your application to your store. -* [**Reducers**](https://redux.js.org/basics/reducers) specify how the application's state changes in response to actions sent to the store. -* [**Redux-Saga**](https://redux-saga.js.org/) is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage. Sagas are created in the `src/store/sagas` folder. -* [**Reselect**](https://github.com/reduxjs/reselect) library is used to create memoized selectors. Selectors are created in the `src/store/selectors` folder. - -#### APIs - -[Axios](https://github.com/axios/axios) is used as an HTTP client library. An Axios instance has exported from `src/api/API.js` file to make HTTP calls with the same config. `src/api` folder also has the API files that have been created for API calls. - -#### Theming - -[Native Base](https://nativebase.io/) is used as UI components library. Native Base components can customize easily. See the [Native Base customize](https://docs.nativebase.io/customizing-components) documentation. We followed the same way. - -* Native Base theme variables are in the `src/theme/variables` folder. -* Native Base component styles are in the `src/theme/components` folder. These files have been generated with Native Base's `ejectTheme` script. -* Styles of components override with the files under the `src/theme/overrides` folder. - -#### Testing - -Unit tests will be created. - -See the [Testing Overview](https://reactjs.org/docs/testing.html) document. - -#### Depended Libraries - -* [Native Base](https://nativebase.io/) is used as UI components library. -* [React Navigation](https://reactnavigation.org/) is used as navigation library. -* [Axios](https://github.com/axios/axios) is used as an HTTP client library. -* [Redux](https://redux.js.org/) is used as state management library. -* [Redux Toolkit](https://redux-toolkit.js.org/) library is used as a toolset for efficient Redux development. -* [Redux-Saga](https://redux-saga.js.org/) is used to manage asynchronous processes. -* [Redux Persist](https://github.com/rt2zz/redux-persist) is used as state persistence. -* [Reselect](https://github.com/reduxjs/reselect) is used to create memoized selectors. -* [i18n-js](https://github.com/fnando/i18n-js) is used as i18n library. -* [expo-font](https://docs.expo.io/versions/latest/sdk/font/) library allows loading fonts easily. -* [Formik](https://github.com/jaredpalmer/formik) is used to build forms. -* [Yup](https://github.com/jquense/yup) is used for form validations. - -## Social / External Logins - -If you want to configure social/external logins for your application, please follow the [Social/External Logins](../../social-external-logins.md) document. - -## What's Next? +```` -- [The getting started document](../../get-started) explains how to create a new application in a few minutes. -- [The application development tutorial](../../tutorials/book-store/part-01.md) explains step by step application development. +ABP Studio provides pre-architected, production-ready templates to jump-start a new solution. One of these templates is the Layered solution template. It is designed for building monolithic layered systems that follow common application patterns based on [Domain-Driven Design](../../framework/architecture/domain-driven-design) (DDD) principles. The template includes multiple layers, integrates existing modules, and provides host applications based on your selections, making it an excellent foundation for your layered system. -## See Also -* [Video tutorial](https://abp.io/video-courses/essentials/app-template) +> **This document explains the Layered solution template in detail. It is a reference document to fully understand the solution and refer to when you have trouble.** +> +> **If you want to quickly create a layered solution, please refer to *[Quick Start: Creating a Layered Web Application with ABP Studio](../../get-started/layered-web-application.md)* document.** + +## Contents + +* [Overview](overview.md) +* [Solution structure](solution-structure.md) +* [Main Components](main-components.md) + * [Web Applications](web-applications.md) + * [Db Migrator](db-migrator.md) + * [Mobile Applications](mobile-applications.md) +* [Built-In Features](built-in-features.md) + * [Authentication](authentication.md) + * [Database configurations](database-configurations.md) + * [Logging (with Serilog)](logging.md) + * [Swagger integration](swagger-integration.md) + * [Multi-Tenancy](multi-tenancy.md) + * [BLOB storing](blob-storing.md) + * [CORS configuration](cors-configuration.md) +* [Helm Charts and Kubernetes](helm-charts-and-kubernetes.md) diff --git a/docs/en/solution-templates/layered-web-application/logging.md b/docs/en/solution-templates/layered-web-application/logging.md new file mode 100644 index 0000000000..1ee3278bfb --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/logging.md @@ -0,0 +1,35 @@ +# Layered Solution: Logging + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Database configurations", + "Path": "solution-templates/layered-web-application/database-configurations" + }, + "Next": { + "Name": "Swagger integration", + "Path": "solution-templates/layered-web-application/swagger-integration" + } +} +``` + +The ABP Studio [layered solution template](index.md) is fully configured for [logging](../../framework/fundamentals/logging.md). All the applications are configured to use the [Serilog](https://serilog.net/) library for structured logging. They are configured in a common way for logging. This document explains that common logging structure. + +## The Serilog Sinks + +The Serilog library is configured so it writes the logs to the following targets (a.k.a. [sinks](https://github.com/serilog/serilog/wiki/Provided-Sinks)) in parallel: + +* **[Console](https://github.com/serilog/serilog-sinks-console)**: Logs are written to the standard output of the executing application. Logging to console is useful when you want to see logs easily while it is running in a container. +* **[File](https://github.com/serilog/serilog-sinks-file)**: Logs are written to a file named `logs.txt` located under the `Logs` folder of the executing application. File logging is useful when you run the application on your local computer. You can check logs easily when you have a trouble. This sinks is only configured for DEBUG mode. It won't be available in your production environment (you can change the behavior in your `Program.cs` file). +* **ABP Studio**: This is a Sink provided by ABP Studio. It sends all logs to ABP Studio, so you can easily monitor your logs in real-time on your ABP Studio Application Monitoring panel. + +The solution can work with [any sink](https://github.com/serilog/serilog/wiki/Provided-Sinks) supported by Serilog. You can add more sinks, remove pre-installed sinks or fine tune their configuration for your solution. + +## Program.cs + +The `Program.cs` file is the main point that configures the logging system. It is done here, because we want to initialize and start the logging in the very beginning of the application. + +## Additional Information + +We are using ABP Serilog Enrichers in the module class of the application. It is done by the `app.UseAbpSerilogEnrichers();` line in the `OnApplicationInitialization` method of your module class. That ASP.NET Core middleware adds current [tenant](../../framework/architecture/multi-tenancy/index.md), [user](../../framework/infrastructure/current-user.md), client and correlation id information to the log records. \ No newline at end of file diff --git a/docs/en/solution-templates/layered-web-application/main-components.md b/docs/en/solution-templates/layered-web-application/main-components.md new file mode 100644 index 0000000000..b7b324069e --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/main-components.md @@ -0,0 +1,21 @@ +# Layered Solution: Main Components + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Solution structure", + "Path": "solution-templates/layered-web-application/solution-structure" + }, + "Next": { + "Name": "Web Applications", + "Path": "solution-templates/layered-web-application/web-applications" + } +} +```` + +The solution consists of various applications; web applications, mobile applications, and a database migrator application. These applications are the main components of the solution and are designed to work together to provide a complete solution. The following documents explains these components in details: + +* [Web Applications](web-applications.md) +* [Db Migrator](db-migrator.md) +* [Mobile Applications](mobile-applications.md) \ No newline at end of file diff --git a/docs/en/solution-templates/layered-web-application/mobile-applications.md b/docs/en/solution-templates/layered-web-application/mobile-applications.md new file mode 100644 index 0000000000..09da244f27 --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/mobile-applications.md @@ -0,0 +1,122 @@ +# Layered Solution: Mobile Applications + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Db Migrator", + "Path": "solution-templates/layered-web-application/db-migrator" + }, + "Next": { + "Name": "Built-In Features", + "Path": "solution-templates/layered-web-application/built-in-features" + } +} +``` + +> You must have an ABP Team or a higher license to be able to create a mobile application project with ABP Studio. + +Mobile applications are an essential part of modern software solutions. They provide a user-friendly interface to the end-users and allow them to access the system from anywhere. ABP Studio allows you to create mobile applications for your layered solution. You can create a new mobile application project, configure it, and run it on your device. + +## Mobile Application Types + +ABP Studio supports the following mobile application types: + +- **None**: No mobile application project is created. It is the default option. +- **MAUI**: Cross-platform mobile applications with .NET MAUI (Multi-platform App UI). You can create MAUI projects with ABP Studio. +- **React Native**: Cross-platform mobile applications that share code between iOS and Android platforms. You can create React Native projects with ABP Studio. + +You can select the mobile application type when creating a new layered application project during the *Mobile Framework* step. + +![mobile-applications](images/mobile-applications.png) + +### The MAUI Application + +This is the mobile application that is built based on Microsoft's [MAUI framework](https://learn.microsoft.com/en-us/dotnet/maui). It will be in the solution only if you've selected the MAUI as your mobile application option. + +#### Project Structure +Entire MAUI application is built on the AppShell pattern of MAUI. You can find the AppShell class in the `Acme.Bookstore.Maui` project. It is the entry point of the application. It is responsible for initializing the application and registering the services. You find all the pages and routing information in the `AppShell.xaml` file. + +- **Pages**: Pages are located in the `Pages` folder of the project. Each page has a XAML & C# file. XAML file is responsible for the UI and C# file is responsible for the initialization of the page. + +- **ViewModels**: ViewModels are located in the `ViewModels` folder of the project. Each ViewModel has a C# file. ViewModels are responsible for the business logic of the pages. + +- **Oidc**: Oidc folder contains the logic for the authentication of the application. It contains the `MauiAuthenticationBrowser` class which manages the authentication process of the application. + +- **Localization**: Localization folder contains the localization logic of the application. It contains regular ABP Localization logic and the `LocalizationResourceManager` class which is wrapper for the ABP localization logic on MAUI. + +- **Messages**: Messages folder contains the message data for the communication inside application. Messages are used to send data between pages and viewmodels. It's designed on the [MVVM Toolkit Messenger](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/messenger) feature. + +- **Storage**: Storage folder contains the storage logic of the application. It contains the `IStorage` class which is wrapper for the [SecureStorage](https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/storage/secure-storage) feature. It is used to store the authentication data of the user and preferences of the application. + +_Rest of the folders are MAUI default folders. You can check the [.NET MAUI single project documentatipon](https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/single-project?view=net-maui-8.0) for more information._ + +#### Running the application +Before running the MAUI Application, rest of the applications in the solution must be running. Such as AuthServer, MobileGateway and the microservices. + +Make sure that you prepared devices for debugging. You can check the following documentation for each platform. + +- [Android](https://learn.microsoft.com/en-us/dotnet/maui/android/emulator/) +- [iOS](https://learn.microsoft.com/en-us/dotnet/maui/ios/pair-to-mac) +- [MacCatalyst](https://learn.microsoft.com/en-us/dotnet/maui/mac-catalyst/cli) +- [Windows](https://learn.microsoft.com/en-us/dotnet/maui/windows/setup) + +##### Network + +All the platforms including iOS, MacCataylst and Windows, runs the applications in the same network of the host. So, you can use the `localhost` address to connect to the applications. + +But in the **Android Emulator**, you need to use the `adb reverse` command to connect to the applications. You can use the following command to connect to the AuthServer application: + +```bash +adb reverse tcp:44300 tcp:44300 +``` + +> `44300` is an example port. You need to change it based on the port of the AuthServer & MobileGateway application. + +> You need to run the command for a running emulator. If you run the emulator after running the command, you need to run the command again. + + +##### Target Framework + +Since MAUI Applications have multiple target frameworks, you need to select the target framework before running the application. You can select the target framework from the context menu of the Solution Runner. + +![ABP Studio MAUI Target Framework](images/solutionrunner-maui-targetframework.png) + +##### Running with ABP Studio +You can start the MAUI application with the solution runner. You can click the start button of the MAUI application in the solution runner tree. It will start the application on the selected target framework. Since they're not running on a process and they're running on a device, you can't see them as running state in the solution runner. After the application is deployed, it'll be opened on the device and it'll be shown as stopped in the solution runner. + +#### Development on MAUI Application + +You can follow [Mobile Application Development Tutorial - MAUI](../../tutorials/mobile/maui/index.md) to learn how to develop on MAUI Application. + +### The React Native Application + +This is the mobile application that is built based on Facebook's [React Native framework](https://reactnative.dev/) and [Expo](https://expo.dev/). It will be in the solution only if you've selected React Native as your mobile application option. + +#### Project Structure +- **Environment.js**: file using for provide application level variables like `apiUrl`, `oAuthConfig` and etc. + +- **api**: The `api` folder contains HTTP request files that simplify API management in the React Native starter template + - `API.js:` exports **axiosInstance**. It provides axios instance filled api url + +- **components**: In the `components` folder you can reach built in react native components that you can use in your app. These components **facilitates** your list, select and etc. operations + +- **contexts**: `contexts` folder contains [react context](https://react.dev/reference/react/createContext). You can expots your contexts in this folder. `Localization context provided in here` + +- **navigators**: folder contains [react-native stacks](https://reactnavigation.org/docs/stack-navigator/). After create new *FeatureName*Navigator we need to provide in `DrawerNavigator.js` file as `Drawer.Screen` + +- **screens**: is the content of navigated page. We'll pass as component property to [Stack.Screen](https://reactnavigation.org/docs/native-stack-navigator/) + +- **store**: folder manages state-management operations. We will define `actions`, `reducers`, `sagas` and `selectors` here. + +- **styles**: folder contains app styles. `system-style.js` comes built in template we can also add new styles. + +- **utils**: folder contains helper functions that we can use in application + +#### Running the Application + +React Native applications can't be run with the solution runner. You need to run them with the React Native CLI. You can check the [React Native documentation](https://reactnative.dev/docs/environment-setup) to learn how to setup the environment for React Native development. + +Before running the React Native application, rest of the applications in the solution must be running. Such as AuthServer, MobileGateway and the microservices. + +Then you can run the React Native application by following this documentation: [Getting Started with the React Native](../../framework/ui/react-native/index.md). \ No newline at end of file diff --git a/docs/en/solution-templates/layered-web-application/multi-tenancy.md b/docs/en/solution-templates/layered-web-application/multi-tenancy.md new file mode 100644 index 0000000000..53727f1c85 --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/multi-tenancy.md @@ -0,0 +1,74 @@ +# Layered Solution: Multi-Tenancy + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Swagger integration", + "Path": "solution-templates/layered-web-application/swagger-integration" + }, + "Next": { + "Name": "BLOB storing", + "Path": "solution-templates/layered-web-application/blob-storing" + } +} +``` + +> Some of the features mentioned in this document may not be available in the free version. We're using the **\*** symbol to indicate that a feature is available in the **[Team](https://abp.io/pricing)** and **[Higher](https://abp.io/pricing)** licenses. + +Multi-tenancy is a software architecture where a single instance(codebase) of software runs on a server and serves multiple tenants. Tenants are isolated from each other and can have their own data, configurations, and users. This document explains how the multi-tenancy mechanism works in the layered solution template. You can learn more about multi-tenancy in the [Multi-Tenancy](../../framework/architecture/multi-tenancy/index.md), [Tenant Management](../../modules/tenant-management.md) and [SaaS **\***](../../modules/saas.md) documents. + +## Multi-Tenancy in Layered Solutions + +The layered solution templates use the *Multi-Tenancy* architecture only if you *Enable Multi-Tenancy **\**** option while creating the solution. + +![saas-module-selection](images/saas-module-selection.png) + +You can use different databases for each tenant or a shared database for some tenants. In the *SaaS **\*** module, you can specify the database connection strings in the [Connection Strings Management Modal](../../modules/saas.md#connection-string). All cached data is isolated by tenant. Each event, background job, and other data is stored with the tenant id. + +You can use the `ICurrentTenant` service to get the current tenant information in your application. + +```csharp +public class MyService : ITransientDependency +{ + private readonly ICurrentTenant _currentTenant; + + public MyService(ICurrentTenant currentTenant) + { + _currentTenant = currentTenant; + } + + public void MyMethod() + { + var tenantId = _currentTenant.Id; + var tenantName = _currentTenant.Name; + } +} +``` + +Additionally, you can use the [DataFilter](../../framework/infrastructure/data-filtering.md#idatafilter-service-enabledisable-data-filters) system to disable the tenant filter and list all data in the same database. + +```csharp +public class MyBookService : ITransientDependency +{ + private readonly IDataFilter _multiTenantFilter; + private readonly IRepository _bookRepository; + + public MyBookService( + IDataFilter multiTenantFilter, + IRepository bookRepository) + { + _multiTenantFilter = multiTenantFilter; + _bookRepository = bookRepository; + } + + public async Task> GetAllBooksIncludingDeletedAsync() + { + //Temporary disable the IMultiTenant filter + using (_multiTenantFilter.Disable()) + { + return await _bookRepository.GetListAsync(); + } + } +} +``` \ No newline at end of file diff --git a/docs/en/solution-templates/layered-web-application/overview.md b/docs/en/solution-templates/layered-web-application/overview.md new file mode 100644 index 0000000000..bfc3f4a465 --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/overview.md @@ -0,0 +1,108 @@ +# Layered Solution: Overview + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Index", + "Path": "solution-templates/layered-web-application/index" + }, + "Next": { + "Name": "Solution Structure", + "Path": "solution-templates/layered-web-application/solution-structure" + } +} +```` + +> Some of the features mentioned in this document may not be available in the free version. We're using the **\*** symbol to indicate that a feature is available in the **[Team](https://abp.io/pricing)** and **[Higher](https://abp.io/pricing)** licenses. + + +In this document, you will learn what the Layered solution template offers to you. + +## Pre-Installed Libraries & Services + +The following **libraries and services** come **pre-installed** and **configured** for both **development** and **production** environments. After creating your solution, you can **modify** or **remove** most of them as needed. + +* **[Autofac](https://autofac.org/)** for [Dependency Injection](../../framework/fundamentals/dependency-injection.md). +* **[Serilog](https://serilog.net/)** with File and Console [logging](../../framework/fundamentals/logging.md) providers. +* **[Redis](https://redis.io/)** for [distributed caching](../../framework/fundamentals/caching.md). Redis is used for distributed caching if you select the *Public Website* **\*** or *Tiered* **\*** option. +* **[Swagger](https://swagger.io/)** for exploring and testing HTTP APIs. +* **[OpenIddict](https://github.com/openiddict/openiddict-core)** as the built-in authentication server. + +## Pre-Configured Features + +The following features are built and pre-configured for you in the solution. + +* **Authentication** is fully configured based on best practices. +* **[Permission](../../framework/fundamentals/authorization.md)** (authorization), **[setting](../../framework/infrastructure/settings.md)**, **[feature](../../framework/infrastructure/features.md)** and the **[localization](../../framework/fundamentals/localization.md)** management systems are pre-configured and ready to use. +* **[Background job system](../../framework/infrastructure/background-jobs/index.md)**. +* **[BLOB storge](../../framework/infrastructure/blob-storing/index.md)** system is installed with the [database provider](../../framework/infrastructure/blob-storing/database.md). +* **On-the-fly database migration** system (services automatically migrated their database schema when you deploy a new version). **\*** +* **[Helm](https://helm.sh/)** charts are included to deploy the solution to **[Kubernetes](https://kubernetes.io/)**. **\*** +* **[Swagger](https://swagger.io/)** authentication is configured to test the authorized HTTP APIs. + +## Fundamental Modules + +The following modules are pre-installed and configured for the solution: + +* **[Account](../../modules/account.md)** to authenticate users (login, register, two factor auth **\***, etc) +* **[Identity](../../modules/identity.md)** to manage roles and users +* **[OpenIddict](../../modules/openiddict.md)** (the core part) to implement the OAuth authentication flows + +In addition these, [Feature Management](../../modules/feature-management.md), [Permission Management](../../modules/permission-management.md) and [Setting Management](../../modules/setting-management.md) modules are pre-installed as they are the fundamental feature modules of the ABP. + +## Optional Modules + +The following modules are optionally included in the solution, so you can select the ones you need: + +* **[Audit Logging](../../modules/audit-logging.md)** +* **[Chat](../../modules/chat.md)** **\*** +* **[File Management](../../modules/file-management.md)** **\*** +* **[GDPR](../../modules/gdpr.md)** **\*** +* **[Language Management](../../modules/language-management.md)** **\*** +* **[OpenIddict (Management UI)](../../modules/openiddict.md)** **\*** +* **[Tenant Management](../../modules/tenant-management.md) (Multi-Tenancy) or [SaaS](../../modules/saas.md)** **\*** +* **[Text Template Management](../../modules/text-template-management.md)** **\*** + +## UI Theme + +The **[LeptonX Lite](../../ui-themes/lepton-x-lite/index.md) or [LeptonX theme](https://leptontheme.com/)** **\*** is pre-configured for the solution. You can select one of the color palettes (System, Light or Dark) as default, while the end-user dynamically change it on the fly. + +## Other Options + +Layered startup template asks for some preferences while creating your solution. + +### Database Providers + +There are two database provider options are provided on a new solution creation: + +* **[Entity Framework Core](../../framework/data/entity-framework-core/index.md)** with SQL Server, MySQL and PostgreSQL DBMS options. You can [switch to anther DBMS](../../framework/data/entity-framework-core/other-dbms.md) manually after creating your solution. +* **[MongoDB](../../framework/data/mongodb/index.md)** + +### UI Frameworks + +The solution comes with a main web application with the following UI Framework options: + +* **None** (doesn't include a web application to the solution) +* **Angular** +* **MVC / Razor Pages UI** +* **Blazor WebAssembly** +* **Blazor Server** +* **Blazor WebApp** +* **MAUI with Blazor (Hybrid)** **\*** + +### The Mobile Application + +If you prefer, the solution includes a mobile application. The mobile application is fully integrated to the system, implements authentication (login) and other ABP features, and includes a few screens that you can use and take as example. The following options are available: + +* **None** (doesn't include a mobile application to the solution) +* **MAUI** **\*** +* **React Native** **\*** + +### Multi-Tenancy & SaaS Module **\*** + +The **[SaaS module](../../modules/saas.md)** is included as an option. When you select it, the **[multi-tenancy](../../framework/architecture/multi-tenancy/index.md)** system is automatically configured. Otherwise, the system will not include any multi-tenancy overhead. + +## See Also + +* [Quick Start: Creating a Layered Web Application with ABP Studio](../../get-started/layered-web-application.md) \ No newline at end of file diff --git a/docs/en/solution-templates/layered-web-application/solution-structure.md b/docs/en/solution-templates/layered-web-application/solution-structure.md new file mode 100644 index 0000000000..680df73a59 --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/solution-structure.md @@ -0,0 +1,216 @@ +# Layered Solution: The Structure + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Overview", + "Path": "solution-templates/layered-web-application/overview" + }, + "Next": { + "Name": "Main Components", + "Path": "solution-templates/layered-web-application/main-components" + } +} +```` + +> Some of the features mentioned in this document may not be available in the free version. We're using the **\*** symbol to indicate that a feature is available in the **[Team](https://abp.io/pricing)** and **[Higher](https://abp.io/pricing)** licenses. + +This document explains the solution and folder structure of ABP Studio's [layered solution template](index.md). + +> This document assumes that you've created a new layered solution by following the *[Quick Start: Creating a Layered Web Application with ABP Studio](../../get-started/layered-web-application.md)* guide. (Choose the *Entity Framework Core* as the database provider.) + +## Understanding the ABP Solution Structure + +When you create a new layered solution, you will see a tree structure similar to the one below in the *Solution Explorer* panel: + +![layered-solution-in-explorer](images/layered-solution-in-explorer.png) + +`Acme.Bookstore` is the main **ABP Studio module** of the solution. It includes two folders: `src` and `test`, as shown in the image above. The `src` folder contains the source code of the solution, which is structured according to [DDD](../../framework/architecture/domain-driven-design/index.md) principles, while the `test` folder holds the unit and integration tests. + +> Refer to the *[Concepts](../../studio/concepts.md)* document for a comprehensive definition of ABP Studio solution, module, and package terms. + +## Exploring the Folder Structure + +You can right-click the root item in the solution explorer (`Acme.Bookstore` for this example) and select the *Open with* -> *Explorer* command to open the folder containing the solution in your file system: + +![open-solution-with-explorer](images/open-solution-with-explorer.png) + +The root folder of the solution will be similar to the following: + +![solution-folders](images/solution-folders.png) + +* `.abpstudio` folder stores your personal preferences for this solution and is excluded from source control (Git ignored). It is created and used by ABP Studio. +* `etc` folder contains additional files for the solution, organized into the following sub-folders: + * `abp-studio` folder holds settings managed by ABP Studio. This folder is included in source control and shared among developers. + * `docker` folder provides docker-compose configurations to easily run infrastructure dependencies (e.g., RabbitMQ, Redis) for the solution on your local machine. + * `helm` folder contains Helm charts and related scripts for deploying the solution to Kubernetes. **\*** +* `src` folder contains the solution's source code, structured according to [DDD](../../framework/architecture/domain-driven-design/index.md) principles. It also includes database migrations and, depending on your project creation options, may include mobile and web application projects. +* `test` folder contains unit and integration tests for the solution. + +## Understanding the Layered Solution Structure + +The diagram below illustrates the application's layers and project dependencies: + +![layered-solution-layers](images/layered-project-dependencies.png) + +### .Domain.Shared Project + +This project contains constants, enums and other objects these are actually a part of the domain layer, but needed to be used by all layers/projects in the solution. + +A `BookType` enum and a `BookConsts` class (which may have some constant fields for the `Book` entity, like `MaxNameLength`) are good candidates for this project. + +* This project has no dependency on other projects in the solution. All other projects depend on this one directly or indirectly. + +### .Domain Project + +This is the domain layer of the solution. It mainly contains [entities, aggregate roots](../../framework/architecture/domain-driven-design/entities.md), [domain services](../../framework/architecture/domain-driven-design/domain-services.md), [value objects](../../framework/architecture/domain-driven-design/value-objects.md), [repository interfaces](../../framework/architecture/domain-driven-design/repositories.md) and other domain objects. + +A `Book` entity, a `BookManager` domain service and an `IBookRepository` interface are good candidates for this project. + +* Depends on the `.Domain.Shared` because it uses constants, enums and other objects defined in that project. + +### .Application.Contracts Project + +This project mainly contains [application service](../../framework/architecture/domain-driven-design/application-services.md) **interfaces** and [Data Transfer Objects](../../framework/architecture/domain-driven-design/data-transfer-objects.md) (DTO) of the application layer. It exists to separate the interface & implementation of the application layer. In this way, the interface project can be shared to the clients as a contract package. + +An `IBookAppService` interface and a `BookCreationDto` class are good candidates for this project. + +* Depends on the `.Domain.Shared` because it may use constants, enums and other shared objects of this project in the application service interfaces and DTOs. + +### .Application Project + +This project contains the [application service](../../framework/architecture/domain-driven-design/application-services.md) **implementations** of the interfaces defined in the `.Application.Contracts` project. + +A `BookAppService` class is a good candidate for this project. + +* Depends on the `.Application.Contracts` project to be able to implement the interfaces and use the DTOs. +* Depends on the `.Domain` project to be able to use domain objects (entities, repository interfaces... etc.) to perform the application logic. + +### .EntityFrameworkCore Project + +This is the integration project for the EF Core. It defines the `DbContext` and implements repository interfaces defined in the `.Domain` project. + +* Depends on the `.Domain` project to be able to reference to entities and repository interfaces. + +> This project is available only if you are using EF Core as the database provider. If you select another database provider, its name will be different. + +### .DbMigrator Project + +This is a console application that simplifies the execution of database migrations on development and production environments. When you run this application, it: + +* Creates the database if necessary. +* Applies the pending database migrations. +* Seeds initial data if needed. + +> This project has its own `appsettings.json` file. So, if you want to change the database connection string, remember to change this file too. + +Especially, seeding initial data is important at this point. ABP has a modular data seed infrastructure. See [its documentation](../../framework/infrastructure/data-seeding.md) for more about the data seeding. + +While creating database & applying migrations seem only necessary for relational databases, this project comes even if you choose a NoSQL database provider (like MongoDB). In that case, it still seeds the initial data which is necessary for the application. + +* Depends on the `.EntityFrameworkCore` project (for EF Core) since it needs to access to the migrations. +* Depends on the `.Application.Contracts` project to be able to access permission definitions, because the initial data seeder grants all permissions to the admin role by default. + +### .HttpApi Project + +This project is used to define your API Controllers. + +Most of the time you don't need to manually define API Controllers since ABP's [Auto API Controllers](../../framework/api-development/auto-controllers.md) feature creates them automagically based on your application layer. However, in case of you need to write API controllers, this is the best place to do it. + +* Depends on the `.Application.Contracts` project to be able to inject the application service interfaces. + +### .HttpApi.Client Project + +This is a project that defines C# client proxies to use the HTTP APIs of the solution. You can share this library to 3rd-party clients, so they can easily consume your HTTP APIs in their Dotnet applications (For other types of applications, they can still use your APIs, either manually or using a tool in their own platform) + +Most of the time you don't need to manually create C# client proxies, thanks to ABP's [Dynamic C# API Clients](../../framework/api-development/dynamic-csharp-clients.md) feature. + +`.HttpApi.Client.ConsoleTestApp` project is a console application created to demonstrate the usage of the client proxies. + +* Depends on the `.Application.Contracts` project to be able to share the same application service interfaces and DTOs with the remote service. + +> You can delete this project & dependencies if you don't need to create C# client proxies for your APIs. + +### .Web Project + +This project contains the User Interface (UI) of the application if you are using ASP.NET Core MVC UI. It contains Razor pages, JavaScript files, CSS files, images and so on... + +This project contains the main `appsettings.json` file that contains the connection string and other configurations of the application. + +* Depends on the `.HttpApi` project since the UI layer needs to use APIs and the application service interfaces of the solution. + +> If you check the source code of the `.Web.csproj` file, you will see the references to the `.Application` and the `.EntityFrameworkCore` projects. +> +> These references are actually not needed while coding your UI layer, because the UI layer normally doesn't depend on the EF Core or the Application layer's implementation. These startup templates are ready for tiered deployment, where the API layer is hosted on a separate server than the UI layer. +> +> However, if you don't choose the `--tiered` option, these references will be in the .Web project to be able to host the Web, API and application layers in a single application endpoint. +> +> This gives you the ability to use domain entities & repositories in your presentation layer. However, this is considered as a bad practice according to DDD. + +### Test Projects + +The solution has multiple test projects, one for each layer: + +* `.Domain.Tests` is used to test the domain layer. +* `.Application.Tests` is used to test the application layer. +* `.EntityFrameworkCore.Tests` is used to test EF Core configuration and custom repositories. +* `.Web.Tests` is used to test the UI (if you are using ASP.NET Core MVC UI). +* `.TestBase` is a base (shared) project for all tests. + +In addition, `.HttpApi.Client.ConsoleTestApp` is a console application (not an automated test project) which demonstrate the usage of HTTP APIs from a .NET application. + +Test projects are prepared for integration testing; + +* It is fully integrated into the ABP and all services in your application. +* It uses SQLite in-memory database for EF Core. For MongoDB, it uses the [EphemeralMongo](https://github.com/asimmon/ephemeral-mongo) library. +* Authorization is disabled, so any application service can be easily used in tests. + +You can still create unit tests for your classes which will be harder to write (because you will need to prepare mock/fake objects), but faster to run (because it only tests a single class and skips all the initialization processes). + +### How to Run? + +You can open the [Solution Runner](../../studio/running-applications.md) panel and start the all applications. The default username is `admin` and the password is `1q2w3E*`. + +![run-solution](images/run-solution.png) + +See [Getting Started With the ASP.NET Core MVC Template](../../get-started/layered-web-application.md) for more information. + +## Tiered Structure **\*** + +If you have selected the ASP.NET Core UI and specified the `--tiered` option, the solution created will be a tiered solution. The purpose of the tiered structure is to be able to **deploy Web applications and HTTP API to different servers**: + +![tiered-solution](images/tiered-solution-servers.png) + +* Browser runs your UI by executing HTML, CSS & JavaScript. +* Web servers host static UI files (CSS, JavaScript, image... etc.) & dynamic components (e.g. Razor pages). It performs HTTP requests to the API server to execute the business logic of the application. +* The API Server hosts the HTTP APIs which then use the application & domain layers of the application to perform the business logic. +* Finally, database server hosts your database. + +> Unless you actually need such a 4-tiered deployment, it's suggested to go with the default structure which is simpler to develop, deploy and maintain. + +The solution structure is shown below: + +![bookstore-solution-tiered](images/bookstore-solution-tiered.png) + +As different from the default structure, two new projects come into play: `.AuthServer` & `.HttpApi.Host`. + +### .AuthServer Project + +This project is used as an authentication server for other projects. `.Web` project uses OpenId Connect Authentication to get identity and access tokens for the current user from the AuthServer. Then uses the access token to call the HTTP API server. HTTP API server uses bearer token authentication to obtain claims from the access token to authorize the current user. + +![tiered-solution-applications](images/tiered-solution-applications-authserver.png) + +ABP uses the [OpenIddict Module](../../modules/openiddict.md) that uses the open-source [OpenIddict-core](https://github.com/openiddict/openiddict-core) library for the authentication between applications. See [OpenIddict documentation](https://documentation.openiddict.com/) for details about the OpenIddict and OpenID Connect protocol. + +It has its own `appsettings.json` that contains database connection and other configurations. + +### .HttpApi.Host Project + +This project is an application that hosts the API of the solution. It has its own `appsettings.json` that contains database connection and other configurations. + +### .Web Project + +Just like the default structure, this project contains the User Interface (UI) of the application. It contains razor pages, JavaScript files, style files, images and so on... + +This project contains an `appsettings.json` file, but this time it does not have a connection string because it never connects to the database. Instead, it mainly contains the endpoint of the remote API server and the authentication server. diff --git a/docs/en/solution-templates/layered-web-application/swagger-integration.md b/docs/en/solution-templates/layered-web-application/swagger-integration.md new file mode 100644 index 0000000000..049c30edc7 --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/swagger-integration.md @@ -0,0 +1,19 @@ +# Layered Solution: Swagger Integration + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Logging (with Serilog)", + "Path": "solution-templates/layered-web-application/logging" + }, + "Next": { + "Name": "Multi-Tenancy", + "Path": "solution-templates/layered-web-application/multi-tenancy" + } +} +``` + +[Swagger](https://swagger.io/) is a tool that helps to create, document, and consume RESTful web services. It provides a user interface to interact with the APIs and also a way to generate client SDKs for the APIs. + +In the [Swagger Integration](../../framework/api-development/swagger.md) document, you can find general information about Swagger integration with ABP Framework. diff --git a/docs/en/solution-templates/layered-web-application/web-applications.md b/docs/en/solution-templates/layered-web-application/web-applications.md new file mode 100644 index 0000000000..8a1b71fda7 --- /dev/null +++ b/docs/en/solution-templates/layered-web-application/web-applications.md @@ -0,0 +1,178 @@ +# Layered Solution: Web Applications + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Main Components", + "Path": "solution-templates/layered-web-application/main-components" + }, + "Next": { + "Name": "Db Migrator", + "Path": "solution-templates/layered-web-application/db-migrator" + } +} +```` + +> Some of the features mentioned in this document may not be available in the free version. We're using the **\*** symbol to indicate that a feature is available in the **[Team](https://abp.io/pricing)** and **[Higher](https://abp.io/pricing)** licenses. + +The web applications are the main user interfaces of the solution. They are the entry points for users to interact with the system. The Layered Solution Template supports the following web applications: + +- **MVC / Razor Pages**: This is an ASP.NET Core MVC application. It is a traditional web application that serves HTML pages to users and is suitable for building web applications with server-side rendering. +- **Angular**: This is an Angular application, a single-page application (SPA) that runs on the client side. It communicates with the server using HTTP requests and is ideal for building modern web applications with rich user interfaces. +- **Blazor UI**: A flexible framework for building web applications with .NET. It supports various hosting models: + - **Blazor WebAssembly**: This is a client-side SPA that runs entirely in the user's browser. It communicates with the server using HTTP requests and is suitable for modern web applications with rich interactivity and offline capabilities. + - **Blazor Server**: This is a server-side SPA that runs on the server and communicates with the client in real time using SignalR. It is ideal for applications requiring constant connectivity and rapid server updates. + - **Blazor WebApp**: This is a combination of Blazor technologies optimized for building hybrid web applications that can leverage both client-side and server-side capabilities. + - **Maui Blazor (Hybrid)** **\***: This enables building cross-platform applications that combine Blazor for the UI with .NET MAUI for native device integration. It is suitable for building apps that work across desktop and mobile platforms. +- **No UI**: This option creates a backend-only solution without a web interface, suitable for scenarios like API-only applications or headless services. + +You can select the web application type that fits your requirements during the solution creation process in the *UI Framework* step. The Layered Solution Template generates the selected web applications with the necessary configurations and integrations. + +![Web Applications](images/web-applications.png) + +## MVC / Razor Pages + +MVC (Model-View-Controller) is a design pattern commonly used for building web applications. Razor Pages, on the other hand, is a page-based programming model designed to make building web applications simpler and more productive. + +When you select the MVC / Razor Pages option in the Layered Solution Template, it generates an ASP.NET Core MVC application named something like `Acme.BookStore.Web`. This application serves as the web interface for your solution, using server-side rendering to deliver dynamic HTML pages to users. + +## Angular + +Angular is a popular front-end framework for building single-page applications (SPAs). It offers a rich set of features for creating modern web applications with dynamic and interactive user interfaces. + +When you select the Angular option in the Layered Solution Template, it generates: +- An Angular application located under the solution's root folder, typically named `angular`. +- An ASP.NET Core application, usually named something like `Acme.Bookstore.HttpApi.Host`. + +The Angular application runs as a client-side SPA in the user's browser and communicates with the server by sending HTTP requests to the `*.HttpApi.Host` application. + +![angular-folder-structure](images/angular-folder-structure.png) + +Each of ABP modules is an NPM package. Some ABP modules are added as a dependency in `package.json`. These modules install with their dependencies. To see all ABP packages, you can run the following command in the `angular` folder: + +```bash +yarn list --pattern abp +``` + +Angular application module structure: + +![Angular template structure diagram](images/angular-template-structure-diagram.png) + +### AppModule + +`AppModule` is the root module of the application. Some of the ABP modules and some essential modules are imported to `AppModule`. + +ABP Config modules have also been imported to `AppModule`ย for initial requirements of the lazy-loadable ABP modules. + +### AppRoutingModule + +There are lazy-loadable ABP modules in the `AppRoutingModule` as routes. + +> Paths of ABP Modules should not be changed. + +You should add `routes` property in the `data` object to add a link on the menu to redirect to your custom pages. + +```js +{ + path: 'dashboard', + loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), + canActivate: [authGuard, permissionGuard], + data: { + routes: { + name: 'ProjectName::Menu:Dashboard', + order: 2, + iconClass: 'fa fa-dashboard', + requiredPolicy: 'ProjectName.Dashboard.Host' + } as ABP.Route + } +} +``` +In the above example; +* If the user is not logged in, authGuard blocks access and redirects to the login page. +* permissionGuard checks the user's permission with the `requiredPolicy` property of the `routes` object. If the user is not authorized to access the page, the 403 page appears. +* The `name` property of `routes` is the menu link label. A localization key can be defined. +* The `iconClass` property of the `routes` object is the menu link icon class. +* The `requiredPolicy` property of the `routes` object is the required policy key to access the page. + +After the above `routes` definition, if the user is authorized, the dashboard link will appear on the menu. + +### Shared Module + +The modules that may be required for all modules have been imported to the `SharedModule`. You should import `SharedModule` to all modules. + +See the [Sharing Modules](https://angular.io/guide/sharing-ngmodules) document. + +### Environments + +The files under the `src/environments` folder have the essential configuration of the application. + +### Home Module + +Home module is an example lazy-loadable module that loads on the root address of the application. + +### Styles + +The required style files are added to the `styles` array in `angular.json`. `AppComponent` loads some style files lazily via `LazyLoadService` after the main bundle is loaded to shorten the first rendering time. + +### Testing + +You should create your tests in the same folder as the file you want to test. + +See the [testing document](https://angular.io/guide/testing). + +### Depended Packages + +* [NG Bootstrap](https://ng-bootstrap.github.io/) is used as UI component library. +* [NGXS](https://www.ngxs.io/) is used as state management library. +* [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) is used to support for OAuth 2 and OpenId Connect (OIDC). +* [Chart.js](https://www.chartjs.org/) is used to create widgets. +* [ngx-validate](https://github.com/ng-turkey/ngx-validate) is used for dynamic validation of reactive forms. + +## Blazor UI + +Blazor is a flexible framework for building web applications with .NET. It supports various hosting models, including Blazor WebAssembly, Blazor Server, Blazor WebApp, and Maui Blazor (Hybrid). + +### Blazor WebAssembly + +Blazor WebAssembly is a client-side SPA that runs entirely in the user's browser. It communicates with the server using HTTP requests and is suitable for modern web applications with rich interactivity and offline capabilities. + +When you select the Blazor WebAssembly option in the Layered Solution Template, it generates: +- A Blazor application located under the solution's root folder, typically named `*.Blazor`, which serves as the main Blazor host project. +- A Blazor client application, named `*.Blazor.Client`, where you can write the client-side (UI logic) code. +- An ASP.NET Core application, named `*.HttpApi.Host`, where the server-side (business logic) code runs. + +The Blazor client application communicates with the server by sending HTTP requests to the `*.HttpApi.Host` application. + +### Blazor Server + +Blazor Server is a server-side SPA that runs on the server and communicates with the client in real time using SignalR. It is ideal for applications requiring constant connectivity and rapid server updates. + +When you select the Blazor Server option in the Layered Solution Template, it generates: +- A Blazor application located under the solution's root folder, typically named `*.Blazor`, which serves as the main Blazor host project. + +### Blazor WebApp + +Blazor WebApp is a combination of Blazor technologies optimized for building hybrid web applications that can leverage both client-side and server-side capabilities. + +When you select the Blazor WebApp option in the Layered Solution Template, it generates: +- A Blazor application located under the solution's root folder, typically named `*.Blazor`, which serves as the main Blazor host project. +- A Blazor client application, named `*.Blazor.Client`, where you can write the client-side (UI logic) code. + +The Blazor client application communicates with the server by sending HTTP requests to the `*.Blazor` application. + +### Maui Blazor (Hybrid) **\*** + +Maui Blazor (Hybrid) enables building cross-platform applications that combine Blazor for the UI with .NET MAUI for native device integration. It is suitable for building apps that work across desktop and mobile platforms. + +When you select the Maui Blazor (Hybrid) option in the Layered Solution Template, it generates: +- A Maui Blazor (Hybrid) application located under the solution's root folder, typically named `*.MauiBlazor`, which serves as the main UI host project. +- An ASP.NET Core application, named `*.HttpApi.Host`, where the server-side (business logic) code runs. + +The Maui Blazor (Hybrid) application communicates with the server by sending HTTP requests to the `*.HttpApi.Host` application. + +## No UI + +This option creates a backend-only solution without a web interface, suitable for scenarios like API-only applications or headless services. + +When you select the No UI option in the Layered Solution Template, it generates an ASP.NET Core application named `*.HttpApi.Host` that serves as the backend API for your solution. diff --git a/docs/en/solution-templates/microservice/grpc-calls.md b/docs/en/solution-templates/microservice/grpc-calls.md index 7e052178c7..e8f9328a46 100644 --- a/docs/en/solution-templates/microservice/grpc-calls.md +++ b/docs/en/solution-templates/microservice/grpc-calls.md @@ -12,6 +12,14 @@ > You must have an ABP Business or a higher license to be able to create a microservice solution. +## What is gRPC? + You can use [gRPC](https://grpc.io) to enable high-performance, low-latency communication between microservices. gRPC, or Google Remote Procedure Call, is an open-source remote procedure call system initially developed by Google. It uses HTTP/2 for transport, Protocol Buffers as the interface description language, and provides features such as authentication, load balancing, and more. -For inter-service communication, gRPC is a great choice because it is faster and more efficient than REST. It is also language-agnostic, meaning you can use it with any programming language that supports gRPC. ABP does not restrict you to use gRPC; you can use it just like normal .NET applications. \ No newline at end of file +## Learning Resources + +The microservice startup template hasn't any configuration related to gRPC communication. You can see the following resources to learn how to implement gRPC calls between your services: + +* [Using gRPC with the ABP Framework](https://abp.io/community/articles/using-grpc-with-the-abp-framework-2dgaxzw3) (ABP Community article) +* [Overview for gRPC on .NET](https://learn.microsoft.com/en-us/aspnet/core/grpc/) (Microsoft's documentation) +* [Code-first gRPC services and clients with .NET](https://learn.microsoft.com/en-us/aspnet/core/grpc/code-first) (Microsoft's documentation) \ No newline at end of file diff --git a/docs/en/solution-templates/microservice/microservices.md b/docs/en/solution-templates/microservice/microservices.md index 1afc56547e..4d2848dc93 100644 --- a/docs/en/solution-templates/microservice/microservices.md +++ b/docs/en/solution-templates/microservice/microservices.md @@ -12,7 +12,7 @@ > You must have an ABP Business or a higher license to be able to create a microservice solution. -The ABP Studio Microservice solution consists of a few microservices at the beginning. It is expected that you [add more microservices](guides/add-new-microservice.md) as your solution grows. This document briefly explains the structure of pre-built microservices in the solution. +The ABP Studio Microservice solution consists of a few microservices at the beginning. It is expected that you [add more microservices](adding-new-microservices.md) as your solution grows. This document briefly explains the structure of pre-built microservices in the solution. The microservice count varies based on the options you've selected during the solution creation. However, the following microservices are always included: @@ -41,7 +41,7 @@ Let's explain the projects: * `Acme.CloudCrm.AdministrationService.Contracts` contains service interfaces and data transfer objects of your service. It is useful to separate contracts. In this way, you can share the *Contracts* package with the clients, so they can easily consume your services. See the [communication](communication.md) document to learn how to do it. * `Acme.CloudCrm.AdministrationService.Tests` contains the unit and integration tests for that microservice. While it is highly suggested to build tests for your services, you can delete that project if you don't want to write tests. -> We haven't applied **layering** for the pre-built microservices, because they don't include much code and no need for such complexity. Microservices should be small (micro!) services, so you typically can manage your codebase in a single project/layer. However, if you want to implement layering for your microservices, you can create a layered microservice by following the *[Adding New Microservice](guides/add-new-microservice.md)* guide. +> We haven't applied **layering** for the pre-built microservices, because they don't include much code and no need for such complexity. Microservices should be small (micro!) services, so you typically can manage your codebase in a single project/layer. However, if you want to implement layering for your microservices, you can create a layered microservice by following the *[Adding New Microservice](adding-new-microservices.md)* guide. Next sections introduces and explains the pre-build services of the solution. diff --git a/docs/en/solution-templates/single-layer-web-application/_index.md b/docs/en/solution-templates/single-layer-web-application/_index.md new file mode 100644 index 0000000000..d5720f5602 --- /dev/null +++ b/docs/en/solution-templates/single-layer-web-application/_index.md @@ -0,0 +1,59 @@ +# Single Layer Application Solution Template + +This template provides a simple solution structure with a single project. This document explains that solution structure in details. + +## Getting Started + +* Follow the [Getting Started guide](../../get-started/single-layer-web-application.md) to create a new solution using this startup solution template. +* Follow the [TODO application tutorial](../../tutorials/todo/single-layer/index.md) to learn how to create a simple application with this startup solution template. + +## The Solution Structure + +If you created your solution with the default options, you will have a .NET solution as shown below: + +![](../../images/bookstore-single-layer-solution-structure.png) + +In the next sections, we will explain the structure based on this example. Your startup solution can be slightly different based on your preferences. + +### Folder Structure + +Since this template provides a single-project solution, we've separated concerns into folders instead of projects. You can see the pre-defined folders as shown below: + +![](../../images/single-layer-folder-structure.png) + +* Define your database mappings (for [EF Core](../../framework/data/entity-framework-core) or [MongoDB](../../framework/data/mongodb) and [repositories](../../framework/architecture/domain-driven-design/repositories.md) in the `Data` folder. +* Define your [entities](../../framework/architecture/domain-driven-design/entities.md) in the `Entities` folder. +* Define your UI localization keys/values in the `Localization` folder. +* Define your UI menu items in the `Menus` folder. +* Define your [object-to-object mapping](../../framework/infrastructure/object-to-object-mapping.md) classes in the `ObjectMapping` folder. +* Define your UI pages (Razor Pages) in the `Pages` folder (create `Controllers` and `Views` folder yourself if you prefer the MVC pattern). +* Define your [application services](../../framework/architecture/domain-driven-design/application-services.md) in the `Services` folder. + +### How to Run? + +Before running the application, you need to create the database and seed the initial data. To do that, you can run the following command in the directory of your project (in the same folder of the `.csproj` file): + +```bash +dotnet run --migrate-database +``` + +This command will create the database and seed the initial data for you. Then you can run the application with any IDE that supports .NET or by running the `dotnet run` command in the directory of your project. The default username is `admin` and the password is `1q2w3E*`. + +> While creating a database & applying migrations seem only necessary for relational databases, you should run this command even if you choose a NoSQL database provider (like MongoDB). In that case, it still seeds the initial data which is necessary for the application. + +### The Angular UI + +If you choose `Angular` as the UI framework, the solution will be separated into two folders: + +* An `angular` folder that contains the Angular UI application, the client-side code. +* An `aspnet-core` folder that contains the ASP.NET Core solution (a single project), the server-side code. + +The server-side is similar to the solution described in the *Solution Structure* section above. This project serves the API, so the Angular application can consume it. + +The client-side application consumes the HTTP APIs as mentioned. You can see the folder structure of the Angular project shown below: + +![](../../images/single-layer-angular-folder-structure.png) + + ## See Also + +* [Video tutorial](https://abp.io/video-courses/essentials/app-template) \ No newline at end of file diff --git a/docs/en/solution-templates/single-layer-web-application/authentication.md b/docs/en/solution-templates/single-layer-web-application/authentication.md new file mode 100644 index 0000000000..17a6f0f2c5 --- /dev/null +++ b/docs/en/solution-templates/single-layer-web-application/authentication.md @@ -0,0 +1,52 @@ +# Single Layer Solution: Authentication + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Built-In Features", + "Path": "solution-templates/layered-web-application/built-in-features" + }, + "Next": { + "Name": "Database configurations in the Single-Layer solution", + "Path": "solution-templates/layered-web-application/database-configurations" + } +} +``` + +> Some of the features mentioned in this document may not be available in the free version. We're using the **\*** symbol to indicate that a feature is available in the **[Team](https://abp.io/pricing)** and **[Higher](https://abp.io/pricing)** licenses. + +The [Single Layer solution template](index.md) is fully configured for authentication. All the services and applications are configured to use the [OpenIddict](https://documentation.openiddict.com) library for authentication. They are configured in a common way for authentication. This document explains that common authentication structure. + +## OpenIddict + +[OpenIddict](https://documentation.openiddict.com) is an open-source library that provides a simple and easy way to implement an OpenID Connect server in your application. ABP has built-in modules ([OpenIddict](../../modules/openiddict.md), [OpenIddict UI **\***](../../modules/openiddict-pro.md)) to integrate OpenIddict into the solution. + +## Initial Data Seeding + +The Single Layer solution template includes an initial data seeding mechanism to create default clients (applications) and scopes for the solution, if necessary (e.g., when using an Angular UI). The `OpenIddictDataSeedContributor` class can be found in the `Data` folder of the host project. If authentication is handled by the UI application(e.g., MVC / Razor Pages), this class is not included. + +The [OpenIddict UI **\***](../../modules/openiddict-pro.md) module is added only if you select it while creating the solution. + +![new-solution-openiddict-module](images/new-solution-openiddict-module.png) + +The OpenIddict UI **\*** module provides a user interface to manage the OpenIddict entities such as applications, scopes, etc. You can manage these entities from the application UI. + +![openiddict-ui](images/openiddict-ui.png) + +### External Providers + +The authentication server handles token generation, validation, and user account management (e.g., login, registration). It uses the [Account](../../modules/account.md) or [Account Pro **\***](../../modules/account-pro.md) module. The [Account Pro **\***](../../modules/account-pro.md) module additionally supports [social logins](../../modules/account-pro.md#social--external-logins) (e.g., Google, Facebook). Social logins can be enabled, disabled, and configured directly from the application's user interface. + +![account-external-provider](images/account-external-provider.png) + +## Authentication Flows + +Applications in the solution use different authentication flows depending on the application type: + +- **MVC UI Web Application**: + Uses the [Hybrid Flow](https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth) (OpenID Connect Authentication) for user authentication. +- **SPA and Swagger Applications**: + Use the [Authorization Code Flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) to authenticate users. + +If the UI is a SPA application (such as an Angular app), the API host uses [JWT Bearer Authentication](https://jwt.io/introduction/) to authorize user actions. \ No newline at end of file diff --git a/docs/en/solution-templates/single-layer-web-application/blob-storing.md b/docs/en/solution-templates/single-layer-web-application/blob-storing.md new file mode 100644 index 0000000000..d316c009c6 --- /dev/null +++ b/docs/en/solution-templates/single-layer-web-application/blob-storing.md @@ -0,0 +1,51 @@ +# Single Layer Solution: BLOB Storing + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Multi-Tenancy", + "Path": "solution-templates/single-layer-web-application/multi-tenancy" + }, + "Next": { + "Name": "CORS Configuration", + "Path": "solution-templates/single-layer-web-application/cors-configuration" + } +} +``` + +> Some of the features mentioned in this document may not be available in the free version. We're using the **\*** symbol to indicate that a feature is available in the **[Team](https://abp.io/pricing)** and **[Higher](https://abp.io/pricing)** licenses. + +This document explains how to store BLOBs (Binary Large Objects) in a single-layer solution. Storing files, images, videos, and other large objects is common in distributed systems. For more details, refer to the [BLOB Storing System](../../framework/infrastructure/blob-storing/index.md) documentation. + +In the single-layer solution template, the [Database Provider](../../framework/infrastructure/blob-storing/database.md) is used to store BLOBs in the database. The `Volo.Abp.BlobStoring.Database.EntityFrameworkCore` or `Volo.Abp.BlobStoring.Database.MongoDB` package provides the required implementations for storing and retrieving BLOBs in the database. This setup is integrated into the single-layer solution template and is used across all related projects. You can modify the database configuration in the `appsettings.json` file of the API project. + +You can use the `IBlobContainer` or `IBlobContainer` service to store and retrieve BLOBs. Here is an example of storing a BLOB: + +```csharp +public class MyService : ITransientDependency +{ + private readonly IBlobContainer _blobContainer; + + public MyService(IBlobContainer blobContainer) + { + _blobContainer = blobContainer; + } + + public async Task SaveBytesAsync(byte[] bytes) + { + await _blobContainer.SaveAsync("my-blob-1", bytes); + } + + public async Task GetBytesAsync() + { + return await _blobContainer.GetAllBytesOrNullAsync("my-blob-1"); + } +} +``` + +## File Management Module + +The *File Management* module is optional and can be added to the solution during the creation process. It provides a user interface for managing folders and files. For more information, see the [File Management *](../../modules/file-management.md) document. + +![file-management](images/file-management-index-page.png) \ No newline at end of file diff --git a/docs/en/solution-templates/single-layer-web-application/built-in-features.md b/docs/en/solution-templates/single-layer-web-application/built-in-features.md new file mode 100644 index 0000000000..2700c1d8dd --- /dev/null +++ b/docs/en/solution-templates/single-layer-web-application/built-in-features.md @@ -0,0 +1,25 @@ +# Single Layer Solution: Built-In Features + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Db Migrator", + "Path": "solution-templates/single-layer-web-application/db-migrator" + }, + "Next": { + "Name": "Authentication", + "Path": "solution-templates/single-layer-web-application/authentication" + } +} +``` + +The Single Layer solution template includes several built-in features to help you get started with your single-layer web application. These features are designed to provide a solid foundation for your application and help you focus on your business logic. This document provides an overview of the built-in features included in the Single Layer solution template. The following documents explain these features in detail: + +* [Authentication](authentication.md) +* [Database configurations](database-configurations.md) +* [Logging (with Serilog)](logging.md) +* [Swagger integration](swagger-integration.md) +* [Multi-Tenancy](multi-tenancy.md) +* [BLOB storing](blob-storing.md) +* [CORS configuration](cors-configuration.md) \ No newline at end of file diff --git a/docs/en/solution-templates/single-layer-web-application/cors-configuration.md b/docs/en/solution-templates/single-layer-web-application/cors-configuration.md new file mode 100644 index 0000000000..f54c73ad79 --- /dev/null +++ b/docs/en/solution-templates/single-layer-web-application/cors-configuration.md @@ -0,0 +1,32 @@ +# Single Layer Solution: CORS Configuration + +```json +//[doc-nav] +{ + "Previous": { + "Name": "BLOB Storing", + "Path": "solution-templates/single-layer-web-application/blob-storing" + } +} +``` + +Cross-Origin Resource Sharing (CORS) is a security feature that allows web applications to make requests to a different domain than the one that served the web page. + +In the single-layer solution template, CORS configuration is applied in the following cases: +- When [Angular](web-applications.md#angular) is selected as the web application type. +- When [Blazor WebAssembly](web-applications.md#blazor-webassembly) is selected as the web application type. +- When [No UI](web-applications.md#no-ui) is selected as the web application type. + +CORS settings are configured in the `appsettings.json` file of the corresponding project. The web application usually serves as the entry point for front-end applications, so it must be set up to accept requests from different origins. + +The default configuration in `appsettings.json` is as follows: + +```json +{ + "App": { + "CorsOrigins": "https://*.MyProjectName.com" + } +} +``` + +You can modify the `CorsOrigins` property to include additional domains or wildcard subdomains as needed for your application. \ No newline at end of file diff --git a/docs/en/solution-templates/single-layer-web-application/database-configurations.md b/docs/en/solution-templates/single-layer-web-application/database-configurations.md new file mode 100644 index 0000000000..ec48fcf6f0 --- /dev/null +++ b/docs/en/solution-templates/single-layer-web-application/database-configurations.md @@ -0,0 +1,190 @@ +# Single Layer Solution: Database configurations + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Authentication", + "Path": "solution-templates/single-layer-web-application/authentication" + }, + "Next": { + "Name": "Logging (with Serilog)", + "Path": "solution-templates/single-layer-web-application/logging" + } +} +``` + +> Some of the features mentioned in this document may not be available in the free version. We're using the **\*** symbol to indicate that a feature is available in the **[Team](https://abp.io/pricing)** and **[Higher](https://abp.io/pricing)** licenses. + +ABP Studio's Single-Layer Solution Template includes pre-configured database settings. This document explains how to manage database configurations in your solution. + +## Connection String + +Connection strings are stored in the `appsettings.json` file and can be customized for different environments by editing this file. [Web Application](web-applications.md) projects use the `Default` connection string by default. + +To update the connection string for the `Default` key, modify the `appsettings.json` file in your project. Connection strings are defined under the `ConnectionStrings` section, as shown below: + +```json +{ + "ConnectionStrings": { + "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=Bookstore;Trusted_Connection=True;TrustServerCertificate=true" + } +} +``` + +## The DbContext Class + +In the Single-Layer Solution Template, the `DbContext` class is defined in the main project. This class manages the database schema and is derived from the `AbpDbContext` class, which offers additional features and configurations. You can customize the `DbContext` class to add new entities, relationships, and configurations. It is located in the `Data` folder of the main project. + +```csharp +public class BookstoreDbContext : AbpDbContext +{ + + public const string DbTablePrefix = "App"; + public const string DbSchema = null; + + public BookstoreDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + builder.ConfigureSettingManagement(); + builder.ConfigureBackgroundJobs(); + builder.ConfigureAuditLogging(); + builder.ConfigureFeatureManagement(); + builder.ConfigurePermissionManagement(); + builder.ConfigureBlobStoring(); + builder.ConfigureIdentityPro(); + builder.ConfigureOpenIddictPro(); + builder.ConfigureGdpr(); + builder.ConfigureLanguageManagement(); + builder.ConfigureSaas(); + builder.ConfigureTextTemplateManagement(); + + /* Configure your own entities here */ + } +} +``` + +### OnModelCreating Method + +The `OnModelCreating` method is used to configure the database schema. It calls the `Configure*` methods of the ABP Framework to configure the database schema for the modules. You can also configure your own tables/entities inside this method. + +```csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + base.OnModelCreating(builder); + + builder.ConfigurePermissionManagement(); + builder.ConfigureSettingManagement(); + builder.ConfigureBackgroundJobs(); + builder.ConfigureAuditLogging(); + builder.ConfigureFeatureManagement(); + builder.ConfigureIdentityPro(); + builder.ConfigureOpenIddictPro(); + builder.ConfigureLanguageManagement(); + builder.ConfigureSaas(); + builder.ConfigureTextTemplateManagement(); + builder.ConfigureGdpr(); + builder.ConfigureCmsKit(); + builder.ConfigureCmsKitPro(); + builder.ConfigureBlobStoring(); + + /* Configure your own tables/entities inside here */ + + //builder.Entity(b => + //{ + // b.ToTable(DbTablePrefix + "YourEntities", DbSchema); + // b.ConfigureByConvention(); //auto configure for the base class props + // //... + //}); +} +``` + +> The `Configure*` methods are extension methods defined in each module's `EntityFrameworkCore` project. These methods are used to configure the database schema for their respective modules. + +### Configuration + +In the `BookstoreModule` class, the `ConfigureEfCore` method is used to configure the database context. It registers the `BookstoreDbContext` class to the [dependency injection](../../framework/fundamentals/dependency-injection.md) system and sets the SQL Server as the default DBMS for the application. + +```csharp +private void ConfigureEfCore(ServiceConfigurationContext context) +{ + context.Services.AddAbpDbContext(options => + { + /* You can remove "includeAllEntities: true" to create + * default repositories only for aggregate roots + * Documentatidon: https://docs.abp.io/en/abp/latest/Entity-Framework-Core#add-default-repositories + */ + options.AddDefaultRepositories(includeAllEntities: true); + }); + + Configure(options => + { + options.Configure(configurationContext => + { + configurationContext.UseSqlServer(); + }); + }); + +} +``` + +## The `IDesignTimeDbContextFactory` Implementation + +The `IDesignTimeDbContextFactory` interface is used to create a `DbContext` instance at design time. It is used by EF Core tools to create migrations and update the database. The `BookstoreDbContextFactory` class implements the `IDesignTimeDbContextFactory` interface to create a `BookstoreMigrationsDbContext` instance. + +```csharp +public class BookstoreDbContextFactory : IDesignTimeDbContextFactory +{ + public BookstoreDbContext CreateDbContext(string[] args) + { + BookstoreEfCoreEntityExtensionMappings.Configure(); + var configuration = BuildConfiguration(); + + var builder = new DbContextOptionsBuilder() + .UseSqlServer(configuration.GetConnectionString("Default")); + + return new BookstoreDbContext(builder.Options); + } + + private static IConfigurationRoot BuildConfiguration() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false); + + return builder.Build(); + } +} +``` + +## SaaS Module: The Tenant Management UI **\*** + +SaaS module provides the necessary UI to set and change connection string for tenants and trigger the database migrations. + +### The Connection String Management Modal + +You can click to the *Database Connection Strings* command in the *Actions* dropdown button for a tenant in the *Tenants* page of the SaaS module: + +![Database Connection Strings](images/database-connection-strings.png) + +It opens the *Database Connection Strings* modal as shown below: + +![Database Connection Strings Modal](images/database-connection-strings-modal.png) + +Here, we can set a *Default connection string* for the tenant. + +When you make the changes and save the dialog, the database is automatically created and migrated. If you later update the connection string (for example if you change the database name), it will also trigger the database migration process again. + +### Manually Applying the Database Migrations + +If you need to manually trigger the database migrations for a specific tenant, click the *Actions* dropdown for the related tenant and select the *Apply Database Migrations* command on the *Tenant Management* page of the SaaS module: + +![Apply Database Migrations](images/apply-database-migrations.png) diff --git a/docs/en/solution-templates/single-layer-web-application/db-migrator.md b/docs/en/solution-templates/single-layer-web-application/db-migrator.md new file mode 100644 index 0000000000..988b16cef6 --- /dev/null +++ b/docs/en/solution-templates/single-layer-web-application/db-migrator.md @@ -0,0 +1,107 @@ +# Single Layer Solution: Db Migrator + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Web Applications", + "Path": "solution-templates/single-layer-web-application/web-applications" + }, + "Next": { + "Name": "Built-In Features", + "Path": "solution-templates/single-layer-web-application/built-in-features" + } +} +```` + +Unlike the Layered solution template, the Single Layer solution template does not include a separate database migrator project. Instead, the main application project handles database migration and seed data operations. The `*.DbMigrator` project is excluded from this template. To manage database migrations and seed data, you can use the `migrate-database.ps1` script in the root directory or run the `dotnet run --migrate-database` command from the main application project directory. + +![Single Layer Solution: Db Migrator](images/single-layer-db-migrator.png) + +After the migration completes, a message will appear in the console. You can verify the success of the migration by checking the database. + +## Database Migration Service + +Under the `Data` folder of the project, the `BookstoreDbMigrationService` class is responsible for database migration and seed data operations. The `MigrateAsync` method is called in the `Program` class to migrate the database when the application starts with the `--migrate-database` argument. + +First, it checks if the database is created and applies the pending migrations. Then, it seeds the initial data using the `SeedAsync` method. + +```csharp +public async Task MigrateAsync() +{ + var initialMigrationAdded = AddInitialMigrationIfNotExist(); + + if (initialMigrationAdded) + { + return; + } + + Logger.LogInformation("Started database migrations..."); + + await MigrateDatabaseSchemaAsync(); + await SeedDataAsync(); + + Logger.LogInformation($"Successfully completed host database migrations."); + + var tenants = await _tenantRepository.GetListAsync(includeDetails: true); + + var migratedDatabaseSchemas = new HashSet(); + foreach (var tenant in tenants) + { + using (_currentTenant.Change(tenant.Id)) + { + if (tenant.ConnectionStrings.Any()) + { + var tenantConnectionStrings = tenant.ConnectionStrings + .Select(x => x.Value) + .ToList(); + + if (!migratedDatabaseSchemas.IsSupersetOf(tenantConnectionStrings)) + { + await MigrateDatabaseSchemaAsync(tenant); + + migratedDatabaseSchemas.AddIfNotContains(tenantConnectionStrings); + } + } + + await SeedDataAsync(tenant); + } + + Logger.LogInformation($"Successfully completed {tenant.Name} tenant database migrations."); + } + + Logger.LogInformation("Successfully completed all database migrations."); + Logger.LogInformation("You can safely end this process..."); +} +``` + +The `BookstoreDbSchemaMigrator` class is used in the `MigrateDatabaseSchemaAsync` method for the database migration process. It is responsible for applying migrations to the database. + +```csharp +public class BookstoreDbSchemaMigrator : ITransientDependency +{ + private readonly IServiceProvider _serviceProvider; + + public BookstoreDbSchemaMigrator( + IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task MigrateAsync() + { + + /* We intentionally resolving the BookstoreDbContext + * from IServiceProvider (instead of directly injecting it) + * to properly get the connection string of the current tenant in the + * current scope. + */ + + await _serviceProvider + .GetRequiredService() + .Database + .MigrateAsync(); + + } +} +``` \ No newline at end of file diff --git a/docs/en/solution-templates/single-layer-web-application/images/account-external-provider.png b/docs/en/solution-templates/single-layer-web-application/images/account-external-provider.png new file mode 100644 index 0000000000..acfefeee7c Binary files /dev/null and b/docs/en/solution-templates/single-layer-web-application/images/account-external-provider.png differ diff --git a/docs/en/solution-templates/single-layer-web-application/images/apply-database-migrations.png b/docs/en/solution-templates/single-layer-web-application/images/apply-database-migrations.png new file mode 100644 index 0000000000..28d5f73786 Binary files /dev/null and b/docs/en/solution-templates/single-layer-web-application/images/apply-database-migrations.png differ diff --git a/docs/en/solution-templates/single-layer-web-application/images/database-connection-strings-modal.png b/docs/en/solution-templates/single-layer-web-application/images/database-connection-strings-modal.png new file mode 100644 index 0000000000..7dc2fd7243 Binary files /dev/null and b/docs/en/solution-templates/single-layer-web-application/images/database-connection-strings-modal.png differ diff --git a/docs/en/solution-templates/single-layer-web-application/images/database-connection-strings.png b/docs/en/solution-templates/single-layer-web-application/images/database-connection-strings.png new file mode 100644 index 0000000000..bc13bbf932 Binary files /dev/null and b/docs/en/solution-templates/single-layer-web-application/images/database-connection-strings.png differ diff --git a/docs/en/solution-templates/single-layer-web-application/images/file-management-index-page.png b/docs/en/solution-templates/single-layer-web-application/images/file-management-index-page.png new file mode 100644 index 0000000000..c19d41f006 Binary files /dev/null and b/docs/en/solution-templates/single-layer-web-application/images/file-management-index-page.png differ diff --git a/docs/en/solution-templates/single-layer-web-application/images/new-solution-openiddict-module.png b/docs/en/solution-templates/single-layer-web-application/images/new-solution-openiddict-module.png new file mode 100644 index 0000000000..5c849d9f8c Binary files /dev/null and b/docs/en/solution-templates/single-layer-web-application/images/new-solution-openiddict-module.png differ diff --git a/docs/en/solution-templates/single-layer-web-application/images/openiddict-ui.png b/docs/en/solution-templates/single-layer-web-application/images/openiddict-ui.png new file mode 100644 index 0000000000..fdbbc4530b Binary files /dev/null and b/docs/en/solution-templates/single-layer-web-application/images/openiddict-ui.png differ diff --git a/docs/en/solution-templates/single-layer-web-application/images/saas-module-selection.png b/docs/en/solution-templates/single-layer-web-application/images/saas-module-selection.png new file mode 100644 index 0000000000..8904c4eaec Binary files /dev/null and b/docs/en/solution-templates/single-layer-web-application/images/saas-module-selection.png differ diff --git a/docs/en/solution-templates/single-layer-web-application/images/single-layer-db-migrator.png b/docs/en/solution-templates/single-layer-web-application/images/single-layer-db-migrator.png new file mode 100644 index 0000000000..08d8319268 Binary files /dev/null and b/docs/en/solution-templates/single-layer-web-application/images/single-layer-db-migrator.png differ diff --git a/docs/en/solution-templates/single-layer-web-application/images/single-layer-solution-folders.png b/docs/en/solution-templates/single-layer-web-application/images/single-layer-solution-folders.png new file mode 100644 index 0000000000..d2cf400a66 Binary files /dev/null and b/docs/en/solution-templates/single-layer-web-application/images/single-layer-solution-folders.png differ diff --git a/docs/en/solution-templates/single-layer-web-application/images/single-layer-solution-in-explorer.png b/docs/en/solution-templates/single-layer-web-application/images/single-layer-solution-in-explorer.png new file mode 100644 index 0000000000..66a47e24e8 Binary files /dev/null and b/docs/en/solution-templates/single-layer-web-application/images/single-layer-solution-in-explorer.png differ diff --git a/docs/en/solution-templates/single-layer-web-application/images/single-layer-solution-in-visual-studio.png b/docs/en/solution-templates/single-layer-web-application/images/single-layer-solution-in-visual-studio.png new file mode 100644 index 0000000000..adcb33247e Binary files /dev/null and b/docs/en/solution-templates/single-layer-web-application/images/single-layer-solution-in-visual-studio.png differ diff --git a/docs/en/solution-templates/single-layer-web-application/images/solution-runner.png b/docs/en/solution-templates/single-layer-web-application/images/solution-runner.png new file mode 100644 index 0000000000..fdc9e28a52 Binary files /dev/null and b/docs/en/solution-templates/single-layer-web-application/images/solution-runner.png differ diff --git a/docs/en/solution-templates/single-layer-web-application/images/web-applications.png b/docs/en/solution-templates/single-layer-web-application/images/web-applications.png new file mode 100644 index 0000000000..5c12767c2f Binary files /dev/null and b/docs/en/solution-templates/single-layer-web-application/images/web-applications.png differ diff --git a/docs/en/solution-templates/single-layer-web-application/index.md b/docs/en/solution-templates/single-layer-web-application/index.md index d5720f5602..e83100e7cf 100644 --- a/docs/en/solution-templates/single-layer-web-application/index.md +++ b/docs/en/solution-templates/single-layer-web-application/index.md @@ -1,59 +1,33 @@ -# Single Layer Application Solution Template - -This template provides a simple solution structure with a single project. This document explains that solution structure in details. - -## Getting Started - -* Follow the [Getting Started guide](../../get-started/single-layer-web-application.md) to create a new solution using this startup solution template. -* Follow the [TODO application tutorial](../../tutorials/todo/single-layer/index.md) to learn how to create a simple application with this startup solution template. - -## The Solution Structure - -If you created your solution with the default options, you will have a .NET solution as shown below: - -![](../../images/bookstore-single-layer-solution-structure.png) - -In the next sections, we will explain the structure based on this example. Your startup solution can be slightly different based on your preferences. - -### Folder Structure - -Since this template provides a single-project solution, we've separated concerns into folders instead of projects. You can see the pre-defined folders as shown below: - -![](../../images/single-layer-folder-structure.png) - -* Define your database mappings (for [EF Core](../../framework/data/entity-framework-core) or [MongoDB](../../framework/data/mongodb) and [repositories](../../framework/architecture/domain-driven-design/repositories.md) in the `Data` folder. -* Define your [entities](../../framework/architecture/domain-driven-design/entities.md) in the `Entities` folder. -* Define your UI localization keys/values in the `Localization` folder. -* Define your UI menu items in the `Menus` folder. -* Define your [object-to-object mapping](../../framework/infrastructure/object-to-object-mapping.md) classes in the `ObjectMapping` folder. -* Define your UI pages (Razor Pages) in the `Pages` folder (create `Controllers` and `Views` folder yourself if you prefer the MVC pattern). -* Define your [application services](../../framework/architecture/domain-driven-design/application-services.md) in the `Services` folder. - -### How to Run? - -Before running the application, you need to create the database and seed the initial data. To do that, you can run the following command in the directory of your project (in the same folder of the `.csproj` file): - -```bash -dotnet run --migrate-database -``` - -This command will create the database and seed the initial data for you. Then you can run the application with any IDE that supports .NET or by running the `dotnet run` command in the directory of your project. The default username is `admin` and the password is `1q2w3E*`. - -> While creating a database & applying migrations seem only necessary for relational databases, you should run this command even if you choose a NoSQL database provider (like MongoDB). In that case, it still seeds the initial data which is necessary for the application. - -### The Angular UI - -If you choose `Angular` as the UI framework, the solution will be separated into two folders: - -* An `angular` folder that contains the Angular UI application, the client-side code. -* An `aspnet-core` folder that contains the ASP.NET Core solution (a single project), the server-side code. - -The server-side is similar to the solution described in the *Solution Structure* section above. This project serves the API, so the Angular application can consume it. - -The client-side application consumes the HTTP APIs as mentioned. You can see the folder structure of the Angular project shown below: - -![](../../images/single-layer-angular-folder-structure.png) - - ## See Also - -* [Video tutorial](https://abp.io/video-courses/essentials/app-template) \ No newline at end of file +# ABP Studio: Single Layer Solution Template + +````json +//[doc-nav] +{ + "Next": { + "Name": "Overview", + "Path": "solution-templates/single-layer-web-application/overview" + } +} +```` + +ABP Studio offers pre-architected, production-ready templates to quickly start a new solution. One of these is the Single Layer solution template, designed for building monolithic systems with minimal layers. It follows [Domain-Driven Design](../../framework/architecture/domain-driven-design) (DDD) principles and common application patterns. The template includes a single project, integrates existing modules, and provides host applications based on your selections, making it a solid foundation for your system. + +> **This document explains the Single Layer solution template in detail. It is a reference document to fully understand the solution and refer to when you have trouble.** +> +> **If you want to quickly create a single-layer solution, please refer to *[Quick Start: Creating a Single Layer Web Application with ABP Studio](../../get-started/single-layer-web-application.md)* document.** + +## Contents + +* [Overview](overview.md) +* [Solution structure](solution-structure.md) +* [Main Components](main-components.md) + * [Web Applications](web-applications.md) + * [Db Migrator](db-migrator.md) +* [Built-In Features](built-in-features.md) + * [Authentication](authentication.md) + * [Database configurations](database-configurations.md) + * [Logging (with Serilog)](logging.md) + * [Swagger integration](swagger-integration.md) + * [Multi-Tenancy](multi-tenancy.md) + * [BLOB storing](blob-storing.md) + * [CORS configuration](cors-configuration.md) \ No newline at end of file diff --git a/docs/en/solution-templates/single-layer-web-application/logging.md b/docs/en/solution-templates/single-layer-web-application/logging.md new file mode 100644 index 0000000000..5b3483822b --- /dev/null +++ b/docs/en/solution-templates/single-layer-web-application/logging.md @@ -0,0 +1,35 @@ +# Single Layer Solution: Logging + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Database configurations", + "Path": "solution-templates/single-layer-web-application/database-configurations" + }, + "Next": { + "Name": "Swagger integration", + "Path": "solution-templates/single-layer-web-application/swagger-integration" + } +} +``` + +The ABP Studio [single-layer solution template](index.md) is fully configured for [logging](../../framework/fundamentals/logging.md). All the applications are configured to use the [Serilog](https://serilog.net/) library for structured logging. They are configured in a common way for logging. This document explains that common logging structure. + +## The Serilog Sinks + +The Serilog library is configured so it writes the logs to the following targets (a.k.a. [sinks](https://github.com/serilog/serilog/wiki/Provided-Sinks)) in parallel: + +* **[Console](https://github.com/serilog/serilog-sinks-console)**: Logs are written to the standard output of the executing application. Logging to console is useful when you want to see logs easily while it is running in a container. +* **[File](https://github.com/serilog/serilog-sinks-file)**: Logs are written to a file named `logs.txt` located under the `Logs` folder of the executing application. File logging is useful when you run the application on your local computer. You can check logs easily when you have a trouble. This sinks is only configured for DEBUG mode. It won't be available in your production environment (you can change the behavior in your `Program.cs` file). +* **ABP Studio**: This is a Sink provided by ABP Studio. It sends all logs to ABP Studio, so you can easily monitor your logs in real-time on your ABP Studio Application Monitoring panel. + +The solution can work with [any sink](https://github.com/serilog/serilog/wiki/Provided-Sinks) supported by Serilog. You can add more sinks, remove pre-installed sinks or fine tune their configuration for your solution. + +## Program.cs + +The `Program.cs` file is the main point that configures the logging system. It is done here, because we want to initialize and start the logging in the very beginning of the application. + +## Additional Information + +We are using ABP Serilog Enrichers in the module class of the application. It is done by the `app.UseAbpSerilogEnrichers();` line in the `OnApplicationInitialization` method of your module class. That ASP.NET Core middleware adds current [tenant](../../framework/architecture/multi-tenancy/index.md), [user](../../framework/infrastructure/current-user.md), client and correlation id information to the log records. \ No newline at end of file diff --git a/docs/en/solution-templates/single-layer-web-application/main-components.md b/docs/en/solution-templates/single-layer-web-application/main-components.md new file mode 100644 index 0000000000..d219c69155 --- /dev/null +++ b/docs/en/solution-templates/single-layer-web-application/main-components.md @@ -0,0 +1,20 @@ +# Single Layer Solution: Main Components + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Solution structure", + "Path": "solution-templates/single-layer-web-application/solution-structure" + }, + "Next": { + "Name": "Web Applications", + "Path": "solution-templates/single-layer-web-application/web-applications" + } +} +```` + +The single-layer solution template is a single project containing all the essential components for building a monolithic application. It supports the `--migrate-database` command-line argument to create the database and seed initial data. The main components of the solution are: + +* [Web Applications](web-applications.md) +* [Db Migrator](db-migrator.md) \ No newline at end of file diff --git a/docs/en/solution-templates/single-layer-web-application/multi-tenancy.md b/docs/en/solution-templates/single-layer-web-application/multi-tenancy.md new file mode 100644 index 0000000000..452cfa9bcd --- /dev/null +++ b/docs/en/solution-templates/single-layer-web-application/multi-tenancy.md @@ -0,0 +1,74 @@ +# Single Layer Solution: Multi-Tenancy + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Swagger integration", + "Path": "solution-templates/single-layer-web-application/swagger-integration" + }, + "Next": { + "Name": "BLOB storing", + "Path": "solution-templates/single-layer-web-application/blob-storing" + } +} +``` + +> Some of the features mentioned in this document may not be available in the free version. We're using the **\*** symbol to indicate that a feature is available in the **[Team](https://abp.io/pricing)** and **[Higher](https://abp.io/pricing)** licenses. + +Multi-tenancy is a software architecture where a single instance(codebase) of software runs on a server and serves multiple tenants. Tenants are isolated from each other and can have their own data, configurations, and users. This document explains how the multi-tenancy mechanism works in the single-layer solution template. You can learn more about multi-tenancy in the [Multi-Tenancy](../../framework/architecture/multi-tenancy/index.md), [Tenant Management](../../modules/tenant-management.md) and [SaaS **\***](../../modules/saas.md) documents. + +## Multi-Tenancy in Single Layer Solutions + +The single-layer solution templates use the *Multi-Tenancy* architecture only if you *Enable Multi-Tenancy **\**** option while creating the solution. + +![saas-module-selection](images/saas-module-selection.png) + +You can use different databases for each tenant or a shared database for some tenants. In the *SaaS **\*** module, you can specify the database connection strings in the [Connection Strings Management Modal](../../modules/saas.md#connection-string). All cached data is isolated by tenant. Each event, background job, and other data is stored with the tenant id. + +You can use the `ICurrentTenant` service to get the current tenant information in your application. + +```csharp +public class MyService : ITransientDependency +{ + private readonly ICurrentTenant _currentTenant; + + public MyService(ICurrentTenant currentTenant) + { + _currentTenant = currentTenant; + } + + public void MyMethod() + { + var tenantId = _currentTenant.Id; + var tenantName = _currentTenant.Name; + } +} +``` + +Additionally, you can use the [DataFilter](../../framework/infrastructure/data-filtering.md#idatafilter-service-enabledisable-data-filters) system to disable the tenant filter and list all data in the same database. + +```csharp +public class MyBookService : ITransientDependency +{ + private readonly IDataFilter _multiTenantFilter; + private readonly IRepository _bookRepository; + + public MyBookService( + IDataFilter multiTenantFilter, + IRepository bookRepository) + { + _multiTenantFilter = multiTenantFilter; + _bookRepository = bookRepository; + } + + public async Task> GetAllBooksIncludingDeletedAsync() + { + //Temporary disable the IMultiTenant filter + using (_multiTenantFilter.Disable()) + { + return await _bookRepository.GetListAsync(); + } + } +} +``` \ No newline at end of file diff --git a/docs/en/solution-templates/single-layer-web-application/overview.md b/docs/en/solution-templates/single-layer-web-application/overview.md new file mode 100644 index 0000000000..8592dc1e8a --- /dev/null +++ b/docs/en/solution-templates/single-layer-web-application/overview.md @@ -0,0 +1,95 @@ +# Single Layer Solution: Overview + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Index", + "Path": "solution-templates/single-layer-web-application/index" + }, + "Next": { + "Name": "Solution Structure", + "Path": "solution-templates/single-layer-web-application/solution-structure" + } +} +```` + +> Some of the features mentioned in this document may not be available in the free version. We're using the **\*** symbol to indicate that a feature is available in the **[Team](https://abp.io/pricing)** and **[Higher](https://abp.io/pricing)** licenses. + +This document explains what the Single-Layer solution template offers. + +## Pre-Installed Libraries & Services + +The following **libraries and services** come **pre-installed** and **configured** for both **development** and **production** environments. After creating your solution, you can **modify** or **remove** most of them as needed. + +* **[Autofac](https://autofac.org/)** for [Dependency Injection](../../framework/fundamentals/dependency-injection.md). +* **[Serilog](https://serilog.net/)** with File and Console [logging](../../framework/fundamentals/logging.md) providers. +* **[Swagger](https://swagger.io/)** for exploring and testing HTTP APIs. +* **[OpenIddict](https://github.com/openiddict/openiddict-core)** as the built-in authentication server. + +## Pre-Configured Features + +The solution comes with the following built-in and pre-configured features: + +* **Authentication** is fully configured based on best practices. +* **[Permission](../../framework/fundamentals/authorization.md)** (authorization), **[setting](../../framework/infrastructure/settings.md)**, **[feature](../../framework/infrastructure/features.md)** and the **[localization](../../framework/fundamentals/localization.md)** management systems are pre-configured and ready to use. +* **[Background job system](../../framework/infrastructure/background-jobs/index.md)**. +* **[BLOB storge](../../framework/infrastructure/blob-storing/index.md)** system is installed with the [database provider](../../framework/infrastructure/blob-storing/database.md). +* **On-the-fly database migration** system (services automatically migrated their database schema when you deploy a new version). **\*** +* **[Swagger](https://swagger.io/)** authentication is configured to test the authorized HTTP APIs. + +## Fundamental Modules + +The following modules are pre-installed and configured for the solution: + +* **[Account](../../modules/account.md)** to authenticate users (login, register, two factor auth **\***, etc) +* **[Identity](../../modules/identity.md)** to manage roles and users +* **[OpenIddict](../../modules/openiddict.md)** (the core part) to implement the OAuth authentication flows + +In addition, [Feature Management](../../modules/feature-management.md), [Permission Management](../../modules/permission-management.md) and [Setting Management](../../modules/setting-management.md) modules are pre-installed as they are the fundamental feature modules of the ABP. + +## Optional Modules + +The following modules are optionally included in the solution, so you can select the ones you need: + +* **[Audit Logging](../../modules/audit-logging.md)** +* **[Chat](../../modules/chat.md)** **\*** +* **[File Management](../../modules/file-management.md)** **\*** +* **[GDPR](../../modules/gdpr.md)** **\*** +* **[Language Management](../../modules/language-management.md)** **\*** +* **[OpenIddict (Management UI)](../../modules/openiddict.md)** **\*** +* **[Tenant Management](../../modules/tenant-management.md) (Multi-Tenancy) or [SaaS](../../modules/saas.md)** **\*** +* **[Text Template Management](../../modules/text-template-management.md)** **\*** + +## UI Theme + +The **[LeptonX Lite](../../ui-themes/lepton-x-lite/index.md) or [LeptonX theme](https://leptontheme.com/)** **\*** is pre-configured for the solution. You can select one of the color palettes (System, Light, or Dark) as default, while the end-user dynamically change it on the fly. + +## Other Options + +Single-layer startup template asks for some preferences while creating your solution. + +### Database Providers + +There are two database provider options are provided on a new solution creation: + +* **[Entity Framework Core](../../framework/data/entity-framework-core/index.md)** with SQL Server, MySQL and PostgreSQL DBMS options. You can [switch to another DBMS](../../framework/data/entity-framework-core/other-dbms.md) manually after creating your solution. +* **[MongoDB](../../framework/data/mongodb/index.md)** + +### UI Frameworks + +The solution comes with a main web application with the following UI Framework options: + +* **None** (doesn't include a UI application to the solution) +* **Angular** +* **MVC / Razor Pages UI** +* **Blazor WebAssembly** +* **Blazor Server** + +### Multi-Tenancy & SaaS Module **\*** + +The **[SaaS module](../../modules/saas.md)** is included as an option. When you select it, the **[multi-tenancy](../../framework/architecture/multi-tenancy/index.md)** system is automatically configured. Otherwise, the system will not include any multi-tenancy overhead. + +## See Also + +* [Quick Start: Creating a Single Layer Web Application with ABP Studio](../../get-started/single-layer-web-application.md) diff --git a/docs/en/solution-templates/single-layer-web-application/solution-structure.md b/docs/en/solution-templates/single-layer-web-application/solution-structure.md new file mode 100644 index 0000000000..45448b46b4 --- /dev/null +++ b/docs/en/solution-templates/single-layer-web-application/solution-structure.md @@ -0,0 +1,71 @@ +# Single Layer Solution: The Structure + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Overview", + "Path": "solution-templates/single-layer-web-application/overview" + }, + "Next": { + "Name": "Main Components", + "Path": "solution-templates/single-layer-web-application/main-components" + } +} +```` + +> Some of the features mentioned in this document may not be available in the free version. We're using the **\*** symbol to indicate that a feature is available in the **[Team](https://abp.io/pricing)** and **[Higher](https://abp.io/pricing)** licenses. + +This document explains the solution and folder structure of ABP Studio's [single layer solution template](index.md). + +> This document assumes that you've created a new single-layer solution by following the *[Quick Start: Creating a Single Layer Web Application with ABP Studio](../../get-started/single-layer-web-application.md)* guide. (Choose the *Entity Framework Core* as the database provider.) + +## Understanding the ABP Solution Structure + +The single-layer solution template is designed to be simple and easy to understand. It includes a single project that contains all the necessary components to build a monolithic application. The solution structure is as follows: + +![single-layer-solution-in-explorer](images/single-layer-solution-in-explorer.png) + +`Acme.Bookstore` is the main **ABP Studio module** in the solution. It also includes the **ABP Studio package** `Acme.Bookstore` as the host application. + +> Refer to the *[Concepts](../../studio/concepts.md)* document for a comprehensive definition of ABP Studio solution, module, and package terms. + +## The Solution Structure + +If you create the solution based on *[Quick Start: Creating a Single Layer Web Application with ABP Studio](../../get-started/single-layer-web-application.md)* guide, the solution structure will be as follows: + +![single-layer-solution-in-visual-studio](images/single-layer-solution-in-visual-studio.png) + +### Folder Structure + +This template uses a single-project structure, with concerns separated into folders instead of projects. The pre-defined folders are shown below: + +![single-layer-solution-folders](images/single-layer-solution-folders.png) + +* **Data**: Define your database mappings (for [EF Core](../../framework/data/entity-framework-core) or [MongoDB](../../framework/data/mongodb) and [repositories](../../framework/architecture/domain-driven-design/repositories.md)) in this folder. +* **Entities**: Define your [entities](../../framework/architecture/domain-driven-design/entities.md) in this folder. +* **Localization**: Define your UI localization keys/values in this folder. +* **Menus**: Define your UI menu items in this folder. +* **Migrations**: Contains the database migration files. It is created automatically by EF Core. +* **ObjectMapping**: Define your [object-to-object mapping](../../framework/infrastructure/object-to-object-mapping.md) classes in this folder. +* **Pages**: Define your UI pages (Razor Pages) in this folder (create `Controllers` and `Views` folders yourself if you prefer the MVC pattern). +* **Permissions**: Define your [permissions](../../framework/fundamentals/authorization.md) in this folder. +* **Services**: Define your [application services](../../framework/architecture/domain-driven-design/application-services.md) in this folder. + +### How to Run? + +When you create a new solution it automatically creates initial migration and run database migrator for you (unless you uncheck these options). However, you can run the following command in the directory of your project (in the same folder of the `.csproj` file) to create the database and seed the initial data: + +```bash +dotnet run --migrate-database +``` + +This command will create the database and seed the initial data for you. Then you can run the application with the ABP Studio [Solution Runner](../../studio/running-applications.md). The default username is `admin` and the password is `1q2w3E*`. + +![solution-runner](images/solution-runner.png) + +> While creating a database & applying migrations seem only necessary for relational databases, you should run this command even if you choose a NoSQL database provider (like MongoDB). In that case, it still seeds the initial data which is necessary for the application. + +## See Also + +* [Video tutorial](https://abp.io/video-courses/essentials/app-template) \ No newline at end of file diff --git a/docs/en/solution-templates/single-layer-web-application/swagger-integration.md b/docs/en/solution-templates/single-layer-web-application/swagger-integration.md new file mode 100644 index 0000000000..bd6ddcf584 --- /dev/null +++ b/docs/en/solution-templates/single-layer-web-application/swagger-integration.md @@ -0,0 +1,19 @@ +# Single Layer Solution: Swagger Integration + +```json +//[doc-nav] +{ + "Previous": { + "Name": "Logging (with Serilog)", + "Path": "solution-templates/single-layer-web-application/logging" + }, + "Next": { + "Name": "Multi-Tenancy", + "Path": "solution-templates/single-layer-web-application/multi-tenancy" + } +} +``` + +[Swagger](https://swagger.io/) is a tool that helps to create, document, and consume RESTful web services. It provides a user interface to interact with the APIs and also a way to generate client SDKs for the APIs. + +In the [Swagger Integration](../../framework/api-development/swagger.md) document, you can find general information about Swagger integration with ABP Framework. diff --git a/docs/en/solution-templates/single-layer-web-application/web-applications.md b/docs/en/solution-templates/single-layer-web-application/web-applications.md new file mode 100644 index 0000000000..94945ba757 --- /dev/null +++ b/docs/en/solution-templates/single-layer-web-application/web-applications.md @@ -0,0 +1,72 @@ +# Single Layer Solution: Web Applications + +````json +//[doc-nav] +{ + "Previous": { + "Name": "Main Components", + "Path": "solution-templates/single-layer-web-application/main-components" + }, + "Next": { + "Name": "Db Migrator", + "Path": "solution-templates/single-layer-web-application/db-migrator" + } +} +```` + +The single-layer solution template includes a web application project that acts as the main application. This ASP.NET Core application hosts the API endpoints and may also serve the user interface, depending on the selected UI framework. + +- **MVC / Razor Pages**: This is an ASP.NET Core MVC application. It is a traditional web application that serves HTML pages to users and is suitable for building web applications with server-side rendering. +- **Angular**: This is an Angular application, a single-page application (SPA) that runs on the client side. It communicates with the server using HTTP requests and is ideal for building modern web applications with rich user interfaces. +- **Blazor UI**: A flexible framework for building web applications with .NET. It supports various hosting models: + - **Blazor WebAssembly**: This is a client-side SPA that runs entirely in the user's browser. It communicates with the server using HTTP requests and is suitable for modern web applications with rich interactivity and offline capabilities. + - **Blazor Server**: This is a server-side SPA that runs on the server and communicates with the client in real time using SignalR. It is ideal for applications requiring constant connectivity and rapid server updates. +- **No UI**: This option creates a backend-only solution without a web interface, suitable for scenarios like API-only applications or headless services. + +You can select the web application type that fits your requirements during the solution creation process in the *UI Framework* step. The single-layer solution template generates the selected web applications with the necessary configurations and integrations. + +![Web Applications](images/web-applications.png) + +## MVC / Razor Pages + +MVC (Model-View-Controller) is a design pattern commonly used for building web applications. Razor Pages, on the other hand, is a page-based programming model designed to make building web applications simpler and more productive. + +When you select the MVC / Razor Pages option in the single-layer solution template, it generates an ASP.NET Core MVC application named something like `Acme.BookStore`. This application serves as the web interface for your solution, using server-side rendering to deliver dynamic HTML pages to users. + +## Angular + +Angular is a popular front-end framework for building single-page applications (SPAs). It offers a rich set of features for creating modern web applications with dynamic and interactive user interfaces. + +When you select the Angular option in the single-layer solution template, it generates: +- An Angular application located under the solution's root folder, typically named `angular`. +- An ASP.NET Core application, usually named something like `Acme.Bookstore`. + +The Angular application runs as a client-side SPA in the user's browser and communicates with the server by sending HTTP requests to the ASP.NET Core host application. + +## Blazor UI + +Blazor is a flexible framework for building web applications with .NET. It supports various hosting models, including Blazor WebAssembly, Blazor Server, Blazor WebApp, and Maui Blazor (Hybrid). + +### Blazor WebAssembly + +Blazor WebAssembly is a client-side SPA that runs entirely in the user's browser. It communicates with the server using HTTP requests and is suitable for modern web applications with rich interactivity and offline capabilities. + +When you select the Blazor WebAssembly option in the Layered Solution Template, it generates: +- A Blazor application located under the solution's root folder, typically named `*.Blazor`, which serves as the main Blazor host project. +- A Blazor client application, named `*.Blazor.Client`, where you can write the client-side (UI logic) code. +- An ASP.NET Core application, named `*.HttpApi.Host`, where the server-side (business logic) code runs. + +The Blazor client application communicates with the server by sending HTTP requests to the `*.HttpApi.Host` application. + +### Blazor Server + +Blazor Server is a server-side SPA that runs on the server and communicates with the client in real time using SignalR. It is ideal for applications requiring constant connectivity and rapid server updates. + +When you select the Blazor Server option in the Layered Solution Template, it generates: +- A Blazor application located under the solution's root folder, typically named `*.Blazor`, which serves as the main Blazor host project. + +## No UI + +This option creates a backend-only solution without a web interface, suitable for scenarios like API-only applications or headless services. + +When you select the No UI option in the Layered Solution Template, it generates an ASP.NET Core application named `*.HttpApi.Host` that serves as the backend API for your solution. diff --git a/docs/en/studio/release-notes.md b/docs/en/studio/release-notes.md index 09ebcb8c0f..24c3437e64 100644 --- a/docs/en/studio/release-notes.md +++ b/docs/en/studio/release-notes.md @@ -2,6 +2,19 @@ This document contains **brief release notes** for each ABP Studio release. Release notes only include **major features** and **visible enhancements**. Therefore, they don't include all the development done in the related version. +## 0.9.18 (2024-12-24) + +* Fixed Blazor WebApp Kubernetes problems. +* Added Visual Studio & Rider options to solution root. +* Fixed problems in blazor-server nolayers template. + +## 0.9.17 (2024-12-17) + +* Added social login option to the "No Layers" Blazor WebAssembly template. +* Fixed AutoMapper missing configuration exception problem during module import. +* Fixed Blazor WebAssembly build issue for the MAUI template. +* Fixed a problem that prevented ABP Studio from opening on macOS. + ## 0.9.16 (2024-12-11) > This version does not work for macOS, we are currently working on that manner. diff --git a/docs/en/studio/version-mapping.md b/docs/en/studio/version-mapping.md index ad9f941d42..aedc55cf7d 100644 --- a/docs/en/studio/version-mapping.md +++ b/docs/en/studio/version-mapping.md @@ -4,6 +4,7 @@ This document provides a general overview of the relationship between various ve | **ABP Studio Version** | **ABP Version of Startup Template** | |------------------------|---------------------------| +| 0.9.17 - 0.9.18 | 9.0.2 | | 0.9.15 - 0.9.16 | 9.0.1 | | 0.9.9 to 0.9.14 | 9.0.0 | | 0.9.8 | 8.3.4 | diff --git a/docs/en/suite/index.md b/docs/en/suite/index.md index ce1c9b3454..1c99ed7198 100644 --- a/docs/en/suite/index.md +++ b/docs/en/suite/index.md @@ -16,4 +16,4 @@ ABP Suite is a complementary tool to the ABP Platform. ABP Suite allows you to b It's a .NET Core Global tool that can be installed from the command line. If you are using [ABP Studio](../studio/index.md), you don't even need to install it because it should already be installed, when you first installed the [ABP Studio](../studio/index.md). -By using the ABP Suite, you can create a new ABP solution, generate CRUD pages from the database to the front-end and directly get a kickstart for your application. ABP Suite is actively developed and new features are being added version by version according to roadmap and your feedbacks. \ No newline at end of file +By using the ABP Suite, you can generate CRUD pages from the database to the front-end and directly get a kickstart for your application. ABP Suite is actively developed and new features are being added version by version according to the roadmap and your feedback. diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app.png b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app.png new file mode 100644 index 0000000000..54860a5e9f Binary files /dev/null and b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/index.md b/docs/en/tutorials/book-store-with-abp-suite/index.md index b0f24ef6d3..492d06bc99 100644 --- a/docs/en/tutorials/book-store-with-abp-suite/index.md +++ b/docs/en/tutorials/book-store-with-abp-suite/index.md @@ -1,11 +1,5 @@ -# Web Application Development (with ABP Suite) Tutorial -````json -//[doc-params] -{ - "UI": ["MVC"], - "DB": ["EF"] -} -```` +# Web Application Development Tutorial (with ABP Suite) + ````json //[doc-nav] { @@ -20,7 +14,7 @@ ## About This Tutorial -> In this tutorial, you will use the [ABP Suite](../../suite/index.md) to generate everything you need to build the **BookStore** application, such as [*Entities*](../../framework/architecture/domain-driven-design/entities.md), [*Domain Services*](../../framework/architecture/domain-driven-design/domain-services.md), [*Application Services*](../../framework/architecture/domain-driven-design/application-services.md), *CRUD pages* and more... +> This tutorial explains how to build code using the [ABP Suite](../../suite/index.md) tool. You will develop an application similar to the one developed in [that tutorial](../book-store/index.md), but this time, the code will be automatically generated instead of manually written. In this tutorial series, you will build an ABP based web application named `Acme.BookStore`. This application is used to manage a list of books and their authors. It is developed using the following technologies: diff --git a/docs/en/tutorials/book-store-with-abp-suite/part-01.md b/docs/en/tutorials/book-store-with-abp-suite/part-01.md index 152d22fb04..8fc6f25fa1 100644 --- a/docs/en/tutorials/book-store-with-abp-suite/part-01.md +++ b/docs/en/tutorials/book-store-with-abp-suite/part-01.md @@ -1,12 +1,5 @@ -# Web Application Development (with ABP Suite) Tutorial - Part 1: Creating the Solution +# Web Application Development Tutorial (with ABP Suite) - Part 1: Creating the Solution -````json -//[doc-params] -{ - "UI": ["MVC"], - "DB": ["EF"] -} -```` ````json //[doc-nav] { diff --git a/docs/en/tutorials/book-store-with-abp-suite/part-02.md b/docs/en/tutorials/book-store-with-abp-suite/part-02.md index bc0f8fd251..3bed0f1b89 100644 --- a/docs/en/tutorials/book-store-with-abp-suite/part-02.md +++ b/docs/en/tutorials/book-store-with-abp-suite/part-02.md @@ -1,11 +1,5 @@ -# Web Application Development (with ABP Suite) Tutorial - Part 2: Creating the Books -````json -//[doc-params] -{ - "UI": ["MVC"], - "DB": ["EF"] -} -```` +# Web Application Development Tutorial (with ABP Suite) - Part 2: Creating the Books + ````json //[doc-nav] { @@ -108,9 +102,9 @@ ABP Suite will generate the necessary code for you. It generates: * All related **permission**, **object mapping** and **navigation menu item** configurations, * and all required **UI components and pages**... -It will take some time to complete the process. After the process is completed, you will see a success message, you can click the *Ok* button, and build & start the application by clicking the *Run -> Build & Start* button in the *Solution Runner* panel: +It will take some time to complete the process. After the process is completed, you will see a success message, you can click the *Ok* button, and then run the application by clicking the *Start* button (or alternatively, directly clicking the *run* icon) in the *Solution Runner* panel: -![](./images/suite-book-entity-6.png) +![](./images/book-store-studio-run-app.png) After the application is started, you can right-click and *Browse* on the application to open it in the ABP Studio's pre-integrated browser. You can see the Books page in the following figure with a single record: diff --git a/docs/en/tutorials/book-store-with-abp-suite/part-03.md b/docs/en/tutorials/book-store-with-abp-suite/part-03.md index d61f43aae5..1fcb84bac7 100644 --- a/docs/en/tutorials/book-store-with-abp-suite/part-03.md +++ b/docs/en/tutorials/book-store-with-abp-suite/part-03.md @@ -1,11 +1,5 @@ -# Web Application Development (with ABP Suite) Tutorial - Part 3: Creating the Authors -````json -//[doc-params] -{ - "UI": ["MVC"], - "DB": ["EF"] -} -```` +# Web Application Development Tutorial (with ABP Suite) - Part 3: Creating the Authors + ````json //[doc-nav] { @@ -62,9 +56,9 @@ You can click the **Save and Generate** button to start the code generation proc ![](./images/suite-book-entity-5.png) -ABP Suite will generate the necessary code for you. It will take some time to complete the process. After the process is completed, you will see a success message, you can click the *Ok* button, and build & start the application by clicking the *Run -> Build & Start* button in the *Solution Runner* panel: +ABP Suite will generate the necessary code for you. It will take some time to complete the process. After the process is completed, you will see a success message, you can click the *Ok* button, and then run the application by clicking the *Start* button (or alternatively, directly clicking the *run* icon) in the *Solution Runner* panel: -![](./images/suite-book-entity-6.png) +![](./images/book-store-studio-run-app.png) After the application is started, you can right-click and *Browse* on the application to open it in the ABP Studio's pre-integrated browser and try to add a new author: diff --git a/docs/en/tutorials/book-store-with-abp-suite/part-04.md b/docs/en/tutorials/book-store-with-abp-suite/part-04.md index aaa0eb3966..fb1583a0c5 100644 --- a/docs/en/tutorials/book-store-with-abp-suite/part-04.md +++ b/docs/en/tutorials/book-store-with-abp-suite/part-04.md @@ -1,11 +1,5 @@ -# Web Application Development (with ABP Suite) Tutorial - Part 4: Book to Author Relation -````json -//[doc-params] -{ - "UI": ["MVC"], - "DB": ["EF"] -} -```` +# Web Application Development Tutorial (with ABP Suite) - Part 4: Book to Author Relation + ````json //[doc-nav] { @@ -55,9 +49,9 @@ After, specifying the metadata, you can click the *Ok* button to close the modal ![](./images/suite-end-of-generation-modal.png) -It will take some time to complete the process. After the process is completed, you will see a success message, you can click the *Ok* button, and build & start the application by clicking the *Run -> Build & Start* button in the *Solution Runner* panel: +It will take some time to complete the process. After the process is completed, you will see a success message, you can click the *Ok* button, and then run the application by clicking the *Start* button (or alternatively, directly clicking the *run* icon) in the *Solution Runner* panel: -![](./images/suite-book-entity-6.png) +![](./images/book-store-studio-run-app.png) After the application is started, you can right-click and *Browse* on the application to open it in the ABP Studio's pre-integrated browser. You can first create an author and then create a book with the author for testing: diff --git a/docs/en/tutorials/book-store-with-abp-suite/part-05.md b/docs/en/tutorials/book-store-with-abp-suite/part-05.md index 842399a80f..8574bcf2eb 100644 --- a/docs/en/tutorials/book-store-with-abp-suite/part-05.md +++ b/docs/en/tutorials/book-store-with-abp-suite/part-05.md @@ -1,11 +1,5 @@ -# Web Application Development (with ABP Suite) Tutorial - Part 5: Customizing the Generated Code -````json -//[doc-params] -{ - "UI": ["MVC"], - "DB": ["EF"] -} -```` +# Web Application Development Tutorial (with ABP Suite) - Part 5: Customizing the Generated Code + ````json //[doc-nav] { diff --git a/docs/en/tutorials/book-store/images/maui-blazor-add-books-component.png b/docs/en/tutorials/book-store/images/maui-blazor-add-books-component.png new file mode 100644 index 0000000000..72cf46e620 Binary files /dev/null and b/docs/en/tutorials/book-store/images/maui-blazor-add-books-component.png differ diff --git a/docs/en/tutorials/book-store/part-01.md b/docs/en/tutorials/book-store/part-01.md index 91700a1a25..51bfcd1b09 100644 --- a/docs/en/tutorials/book-store/part-01.md +++ b/docs/en/tutorials/book-store/part-01.md @@ -2,7 +2,7 @@ ````json //[doc-params] { - "UI": ["MVC","Blazor","BlazorServer", "BlazorWebApp","NG"], + "UI": ["MVC","Blazor","BlazorServer", "BlazorWebApp","NG","MAUIBlazor"], "DB": ["EF","Mongo"] } ```` diff --git a/docs/en/tutorials/book-store/part-02.md b/docs/en/tutorials/book-store/part-02.md index 3ff19437cd..43fcb55d47 100644 --- a/docs/en/tutorials/book-store/part-02.md +++ b/docs/en/tutorials/book-store/part-02.md @@ -2,7 +2,7 @@ ````json //[doc-params] { - "UI": ["MVC","Blazor","BlazorServer", "BlazorWebApp", "NG"], + "UI": ["MVC","Blazor","BlazorServer", "BlazorWebApp", "NG", "MAUIBlazor"], "DB": ["EF","Mongo"] } ```` @@ -518,14 +518,17 @@ Now you can see the final result on your browser: ## Create a Books Page -It's time to create something visible and usable! Right click on the `Pages` folder under the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add a new **razor component**, named `Books.razor`: +It's time to create something visible and usable! Right click on the `Pages` folder under the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else if UI == "Blazor" || UI == "BlazorWebApp" }}`Acme.BookStore.Blazor.Client`{{else}} `Acme.BookStore.MauiBlazor` {{ end }} project and add a new **razor component**, named `Books.razor`: {{ if UI == "Blazor" || UI == "BlazorWebApp" }} ![blazor-add-books-component](images/blazor-add-books-component-client.png) -{{ else }} +{{ else if UI == "BlazorServer" }} ![blazor-add-books-component](images/blazor-add-books-component.png) +{{ else if UI == "MAUIBlazor" }} +![maui-blazor-add-books-component](images/maui-blazor-add-books-component.png) {{ end }} + Replace the contents of this component as shown below: ````html @@ -540,7 +543,7 @@ Replace the contents of this component as shown below: ### Add the Books Page to the Main Menu -Open the `BookStoreMenuContributor` class in the {{ if UI == "BlazorServer"}}`Acme.BookStore.Blazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project add the following code to the end of the `ConfigureMainMenuAsync` method: +Open the `BookStoreMenuContributor` class in the {{ if UI == "BlazorServer"}}`Acme.BookStore.Blazor`{{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project add the following code to the end of the `ConfigureMainMenuAsync` method: ````csharp context.Menu.AddItem( diff --git a/docs/en/tutorials/book-store/part-03.md b/docs/en/tutorials/book-store/part-03.md index fc4e851b94..45444ee76f 100644 --- a/docs/en/tutorials/book-store/part-03.md +++ b/docs/en/tutorials/book-store/part-03.md @@ -2,7 +2,7 @@ ````json //[doc-params] { - "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG"], + "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG", "MAUIBlazor"], "DB": ["EF","Mongo"] } ```` @@ -1101,7 +1101,7 @@ Clicking the "Delete" action calls the `delete` method which then shows a confir {{end}} -{{if UI == "Blazor" || UI == "BlazorServer" || UI == "BlazorWebApp"}} +{{if UI == "Blazor" || UI == "BlazorServer" || UI == "BlazorWebApp" || UI == "MAUIBlazor"}} ## Creating a New Book @@ -1292,13 +1292,13 @@ We can now define a modal to edit the book. Add the following code to the end of The base `AbpCrudPageBase` uses the [object to object mapping](../../framework/infrastructure/object-to-object-mapping.md) system to convert an incoming `BookDto` object to a `CreateUpdateBookDto` object. So, we need to define the mapping. -Open the `BookStoreBlazorAutoMapperProfile` inside the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and change the content as the following: +Open the `BookStoreBlazorAutoMapperProfile` inside the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor` {{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor` {{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and change the content as the following: ````csharp using Acme.BookStore.Books; using AutoMapper; -{{ if UI == "BlazorServer" }}namespace Acme.BookStore.Blazor;{{ else }}namespace Acme.BookStore.Blazor.Client;{{ end }} +{{ if UI == "BlazorServer" }}namespace Acme.BookStore.Blazor; {{ else if UI == "MAUIBlazor" }}namespace Acme.BookStore.MauiBlazor; {{ else }}namespace Acme.BookStore.Blazor.Client;{{ end }} public class BookStoreBlazorAutoMapperProfile : Profile { diff --git a/docs/en/tutorials/book-store/part-04.md b/docs/en/tutorials/book-store/part-04.md index 4ab950b0e1..fba15db253 100644 --- a/docs/en/tutorials/book-store/part-04.md +++ b/docs/en/tutorials/book-store/part-04.md @@ -2,7 +2,7 @@ ````json //[doc-params] { - "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG"], + "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG", "MAUIBlazor"], "DB": ["EF","Mongo"] } ```` diff --git a/docs/en/tutorials/book-store/part-05.md b/docs/en/tutorials/book-store/part-05.md index d4815c5a96..0acc5cac9f 100644 --- a/docs/en/tutorials/book-store/part-05.md +++ b/docs/en/tutorials/book-store/part-05.md @@ -2,7 +2,7 @@ ````json //[doc-params] { - "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG"], + "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG", "MAUIBlazor"], "DB": ["EF","Mongo"] } ```` @@ -389,11 +389,11 @@ Open the `/src/app/book/book.component.html` file and replace the edit and delet * Added `*abpPermission="'BookStore.Books.Edit'"` that hides the edit action if the current user has no editing permission. * Added `*abpPermission="'BookStore.Books.Delete'"` that hides the delete action if the current user has no delete permission. -{{else if UI == "Blazor" || UI == "BlazorServer" || UI == "BlazorWebApp"}} +{{else if UI == "Blazor" || UI == "BlazorServer" || UI == "BlazorWebApp" || UI == "MAUIBlazor"}} ### Authorize the Razor Component -Open the `/Pages/Books.razor` file in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add an `Authorize` attribute just after the `@page` directive and the following namespace imports (`@using` lines), as shown below: +Open the `/Pages/Books.razor` file in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor` {{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor` {{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add an `Authorize` attribute just after the `@page` directive and the following namespace imports (`@using` lines), as shown below: ````html @page "/books" @@ -481,7 +481,7 @@ You can run and test the permissions. Remove a book related permission from the Even we have secured all the layers of the book management page, it is still visible on the main menu of the application. We should hide the menu item if the current user has no permission. -Open the `BookStoreMenuContributor` class in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project, find the code block below: +Open the `BookStoreMenuContributor` class in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project, find the code block below: ````csharp context.Menu.AddItem( diff --git a/docs/en/tutorials/book-store/part-06.md b/docs/en/tutorials/book-store/part-06.md index 05e1d04460..d24b68f569 100644 --- a/docs/en/tutorials/book-store/part-06.md +++ b/docs/en/tutorials/book-store/part-06.md @@ -2,7 +2,7 @@ ````json //[doc-params] { - "UI": ["MVC","Blazor","BlazorServer", "BlazorWebApp", "NG"], + "UI": ["MVC","Blazor","BlazorServer", "BlazorWebApp", "NG", "MAUIBlazor"], "DB": ["EF","Mongo"] } ```` diff --git a/docs/en/tutorials/book-store/part-07.md b/docs/en/tutorials/book-store/part-07.md index b8aa17ace8..0708ecfcb9 100644 --- a/docs/en/tutorials/book-store/part-07.md +++ b/docs/en/tutorials/book-store/part-07.md @@ -2,7 +2,7 @@ ````json //[doc-params] { - "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG"], + "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG", "MAUIBlazor"], "DB": ["EF","Mongo"] } ```` diff --git a/docs/en/tutorials/book-store/part-08.md b/docs/en/tutorials/book-store/part-08.md index 679b6d84ca..a7d73f0209 100644 --- a/docs/en/tutorials/book-store/part-08.md +++ b/docs/en/tutorials/book-store/part-08.md @@ -2,7 +2,7 @@ ````json //[doc-params] { - "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG"], + "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG", "MAUIBlazor"], "DB": ["EF","Mongo"] } ```` diff --git a/docs/en/tutorials/book-store/part-09.md b/docs/en/tutorials/book-store/part-09.md index d481dbe92e..5bf9f90af1 100644 --- a/docs/en/tutorials/book-store/part-09.md +++ b/docs/en/tutorials/book-store/part-09.md @@ -2,7 +2,7 @@ ````json //[doc-params] { - "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG"], + "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG", "MAUIBlazor"], "DB": ["EF","Mongo"] } ```` @@ -848,13 +848,13 @@ That's all! This is a fully working CRUD page, you can create, edit and delete a {{end}} -{{if UI == "Blazor" || UI == "BlazorServer" || UI == "BlazorWebApp"}} +{{if UI == "Blazor" || UI == "BlazorServer" || UI == "BlazorWebApp" || UI == "MAUIBlazor"}} ## The Author Management Page ### Authors Razor Component -Create a new Razor Component Page, `/Pages/Authors.razor`, in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project with the following content: +Create a new Razor Component Page, `/Pages/Authors.razor`, in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project with the following content: ````xml @page "/authors" @@ -1055,7 +1055,7 @@ using Blazorise.DataGrid; using Microsoft.AspNetCore.Authorization; using Volo.Abp.Application.Dtos; -{{ if UI == "BlazorServer" }}namespace Acme.BookStore.Blazor.Pages;{{ else }}namespace Acme.BookStore.Blazor.Client.Pages;{{ end }} +{{ if UI == "BlazorServer" }}namespace Acme.BookStore.Blazor.Pages;{{ else if UI == "MAUIBlazor" }}namespace Acme.BookStore.MauiBlazor.Pages;{{ else }}namespace Acme.BookStore.Blazor.Client.Pages;{{ end }} public partial class Authors { @@ -1201,7 +1201,7 @@ This class typically defines the properties and methods used by the `Authors.raz `Authors` class uses the `IObjectMapper` in the `OpenEditAuthorModal` method. So, we need to define this mapping. -Open the `BookStoreBlazorAutoMapperProfile.cs` in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add the following mapping code in the constructor: +Open the `BookStoreBlazorAutoMapperProfile.cs` in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add the following mapping code in the constructor: ````csharp CreateMap(); @@ -1211,7 +1211,7 @@ You will need to declare a `using Acme.BookStore.Authors;` statement to the begi ### Add to the Main Menu -Open the `BookStoreMenuContributor.cs` in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add the following code to the end of the `ConfigureMainMenuAsync` method: +Open the `BookStoreMenuContributor.cs` in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add the following code to the end of the `ConfigureMainMenuAsync` method: ````csharp context.Menu.AddItem(new ApplicationMenuItem( diff --git a/docs/en/tutorials/book-store/part-10.md b/docs/en/tutorials/book-store/part-10.md index c4383aa5c9..d4137e3681 100644 --- a/docs/en/tutorials/book-store/part-10.md +++ b/docs/en/tutorials/book-store/part-10.md @@ -2,7 +2,7 @@ ````json //[doc-params] { - "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG"], + "UI": ["MVC","Blazor","BlazorServer","BlazorWebApp","NG", "MAUIBlazor"], "DB": ["EF","Mongo"] } ```` @@ -105,7 +105,7 @@ migrationBuilder.AddForeignKey( * Creates an index on the `AuthorId` field. * Declares the foreign key to the `AppAuthors` table. -> If you are using Visual Studio, you may want to use `Add-Migration Added_AuthorId_To_Book -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 `Add-Migration Added_AuthorId_To_Book -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" || UI=="BlazorWebApp"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG" || UI=="MAUIBlazor"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC. {{end}} @@ -1071,11 +1071,11 @@ That's all. Just run the application and try to create or edit an author. {{end}} -{{if UI == "Blazor" || UI == "BlazorServer" || UI == "BlazorWebApp" }} +{{if UI == "Blazor" || UI == "BlazorServer" || UI == "BlazorWebApp" || UI == "MAUIBlazor" }} ### The Book List -It is very easy to show the *Author Name* in the book list. Open the `/Pages/Books.razor` file in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add the following `DataGridColumn` definition just after the `Name` (book name) column: +It is very easy to show the *Author Name* in the book list. Open the `/Pages/Books.razor` file in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor` {{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor` {{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add the following `DataGridColumn` definition just after the `Name` (book name) column: ````xml *Build & Start* button in the *Solution Runner* panel. +We can now start the `CloudCrm.CatalogService` application by clicking the *Start* button (or alternatively, directly clicking the *run* icon) in the *Solution Runner* panel. ![abp-studio-browse-catalog-service-2](images/abp-studio-browse-catalog-service-2.png) @@ -89,7 +89,7 @@ It will open the *Generate C# Proxies* window. Select the `CloudCrm.CatalogServi ![abp-studio-generate-proxy-window](images/abp-studio-generate-proxy-window.png) -> To be able to select the *Application*, you must *Build & Start* the related application beforehand. You can start the application using [Solution Runner](../../studio/running-applications.md) as explained in the previous parts. +> To be able to select the *Application*, you must *Start* the related application beforehand. You can start the application using [Solution Runner](../../studio/running-applications.md) as explained in the previous parts. Lastly, we need to configure the use of a static HTTP client for the `CatalogService` in the `CloudCrm.Web` project. Open the `CloudCrmWebModule.cs` file in the `Web` project and add the following line to the `ConfigureServices` method: @@ -107,9 +107,9 @@ public override void ConfigureServices(ServiceConfigurationContext context) ### Running the Application -Now, stop any application running in the *Solution Runner* panel, and then run the applications by clicking the *Run* -> *Build & Start All* button on the root item in the *Solution Runner* panel: +Now, stop any application running in the *Solution Runner* panel, and then run the applications by clicking the *Start All* button on the root item in the *Solution Runner* panel: -![abp-studio-run-build-and-start-all](images/abp-studio-run-build-and-start-all.png) +![abp-studio-run-build-and-start-all](images/abp-studio-run-start-all.png) After the application is started, you can right-click and [Browse](../../studio/running-applications.md#monitoring) on the `CloudCrm.Web` application to open it in the ABP Studio's pre-integrated browser: @@ -117,6 +117,8 @@ After the application is started, you can right-click and [Browse](../../studio/ > If you can't see the *Products* menu item, you need to grant the `CatalogService` *Product* permission to the *admin* role. You can do this by navigating to *Identity Management* -> *Roles* and editing the *admin* role. Alternatively, you can restart the *CloudCrm.AdministrationService* application to automatically seed all permissions for the *admin* role. +> When we create `Catalog` microservice, the `CatalogService` API scope is also created automatically if the "Enable integration" option is selected. You can verify the new scope in the `CloudCrm.IdentityService` module (.NET solution), in the `CloudCrm.IdentityService` project within the `OpenIddictDataSeeder` class's `CreateApiScopesAsync` method. If you are already logged in to the application, you may need to log out and log back in to reauthorize with the newly created API scope. + You can open the Sql Server Management Studio to see the created tables and data: ![sql-server-management-studio-products](images/sql-server-management-studio-products.png) diff --git a/docs/en/tutorials/microservice/part-05.md b/docs/en/tutorials/microservice/part-05.md index 249cdc8890..f2cefce256 100644 --- a/docs/en/tutorials/microservice/part-05.md +++ b/docs/en/tutorials/microservice/part-05.md @@ -118,7 +118,7 @@ In this code snippet, we configure the `Order` entity to use the `Orders` table Now, we can add a new database migration. You can use Entity Framework Core's `Add-Migration` (or `dotnet ef migrations add`) terminal command, but in this tutorial, we will use ABP Studio's shortcut UI. -Ensure that the solution has built. You can right-click the `CloudCrm.OrderingService` (under the `services` folder) on ABP Studio *Solution Explorer* and select the *Dotnet CLI* -> *Graph Build* command. +Please stop the applications if they are running and ensure that the solution has built. You can right-click the `CloudCrm.OrderingService` (under the `services` folder) on ABP Studio *Solution Explorer* and select the *Dotnet CLI* -> *Graph Build* command. Right-click the `CloudCrm.OrderingService` package and select the *EF Core CLI* -> *Add Migration* command: @@ -217,7 +217,7 @@ namespace CloudCrm.OrderingService.Services; public class OrderAppService : ApplicationService, IOrderAppService { - private readonly IRepository _orderRepository; + private readonly IRepository _orderRepository; public OrderAppService(IRepository orderRepository) { @@ -267,7 +267,7 @@ public class OrderingServiceApplicationAutoMapperProfile : Profile ## Testing the Application Service -Now, we can test the `OrderAppService` class using the Swagger UI. Open the Solution Runner and right-click to `CloudCrm.OrderingService` project and select the *Run* -> *Build & Start* command. After the application starts, you can open the Swagger UI by clicking to the [Browse](../../studio/running-applications.md#monitoring) command: +Now, we can test the `OrderAppService` class using the Swagger UI. Open the Solution Runner and right-click to `CloudCrm.OrderingService` project and select the *Start* command. After the application starts, you can open the Swagger UI by clicking to the [Browse](../../studio/running-applications.md#monitoring) command: ![ordering-service-swagger-ui](images/ordering-service-swagger-ui.png) @@ -411,9 +411,9 @@ private static async Task ConfigureMainMenuAsync(MenuConfigurationContext contex ## Building and Running the Application -Now, we can build and run the application to see the changes. Please stop the applications if they are running. Then open the *Solution Runner* panel, right-click the `CloudCrm` root item, and select the *Run* -> *Build & Start* command: +Now, we can run the application to see the changes. Please stop the applications if they are running. Then open the *Solution Runner* panel, right-click the `CloudCrm` root item, and select the *Start* command: -![abp-studio-run-build-start](images/abp-studio-run-build-start.png) +![abp-studio-run-build-start](images/abp-studio-run-start-all.png) After the applications are started, you can *Browse* and navigate to the `Orders` page to see the list of orders: diff --git a/docs/en/tutorials/microservice/part-06.md b/docs/en/tutorials/microservice/part-06.md index f171767260..af05bbd792 100644 --- a/docs/en/tutorials/microservice/part-06.md +++ b/docs/en/tutorials/microservice/part-06.md @@ -103,24 +103,6 @@ public class ProductIntegrationService : ApplicationService, IProductIntegration > Here, we directly used `List` classes, but instead, you could wrap inputs and outputs into [DTOs](../../framework/architecture/domain-driven-design/data-transfer-objects.md). In that way, it can be possible to add new properties to these DTOs without changing the signature of your integration service method (and without introducing breaking changes for your client applications). -### Exposing the Integration Service as an API - -Integration services are not exposed as HTTP APIs by default. However, you can expose them as HTTP APIs if you need to. To do this, you should configure the `AbpAspNetCoreMvcOptions` in the `ConfigureServices` method of the `CloudCrmCatalogServiceModule`. Open the `CloudCrm.CatalogService` project and locate the `CloudCrmCatalogServiceModule` class. Add the following code to the `ConfigureServices` method: - -```csharp -public override void ConfigureServices(ServiceConfigurationContext context) -{ - // Other configurations... - - Configure(options => - { - options.ExposeIntegrationServices = true; - }); -} -``` - -This code configures the `AbpAspNetCoreMvcOptions` to expose integration services as HTTP APIs. This is useful when you need to call the integration service from a different service using HTTP. You can learn more about this in the [Integration Services](../../framework/api-development/integration-services.md#exposing-integration-services) document. - ## Consuming the Products Integration Service Now that we have created the `IProductIntegrationService` interface and the `ProductIntegrationService` class, we can consume this service from the Ordering service. @@ -161,7 +143,7 @@ namespace CloudCrm.OrderingService.Services; public class OrderAppService : ApplicationService, IOrderAppService { - private readonly IRepository _orderRepository; + private readonly IRepository _orderRepository; private readonly IProductIntegrationService _productIntegrationService; public OrderAppService( @@ -252,7 +234,7 @@ Let's explain the changes we made: ### Generating Proxy Classes for the Integration Service -We have created the `IProductIntegrationService` interface and the `ProductIntegrationService` class in the `CloudCrm.CatalogService` solution. Now, we need to generate the proxy classes for the integration service in the `CloudCrm.OrderingService` package. First, *Build & Start* the `CloudCrm.CatalogService` application in ABP Studio *Solution Runner*. Then, open the *Solution Explorer* and right-click on the `CloudCrm.OrderingService` package. Select the *ABP CLI* -> *Generate Proxy* -> *C#* command: +We have created the `IProductIntegrationService` interface and the `ProductIntegrationService` class in the `CloudCrm.CatalogService` solution. Now, we need to generate the proxy classes for the integration service in the `CloudCrm.OrderingService` package. First, *Start* the `CloudCrm.CatalogService` application in ABP Studio *Solution Runner*. Then, open the *Solution Explorer* and right-click on the `CloudCrm.OrderingService` package. Select the *ABP CLI* -> *Generate Proxy* -> *C#* command: ![generate-proxy-catalog-service](images/generate-proxy-catalog-service.png) @@ -274,6 +256,19 @@ We have generated the proxy classes for the `IProductIntegrationService` interfa > **BaseUrl** refers to the base URL of the Catalog service. You can use the *Copy Url* option from the Catalog service's context menu in the ABP Studio **Solution Runner** to paste it here. +Lastly, open the `CloudCrmOrderingServiceModule` class (the `CloudCrmOrderingServiceModule.cs` file under the `CloudCrm.OrderingService` project of the `CloudCrm.OrderingService` .NET solution) and add the following code to the `ConfigureServices` method: + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + // Other configurations... + context.Services.AddStaticHttpClientProxies( + typeof(CloudCrmCatalogServiceContractsModule).Assembly, + "CatalogService"); +} + +``` + ### Updating the UI to Display the Product Name Open the `Index.cshtml` file (the `Index.cshtml` file under the `Pages/Orders` folder of the `CloudCrm.Web` project of the `CloudCrm.Web` .NET solution) and update the table content to display the product name instead of the product ID: @@ -300,7 +295,7 @@ Open the `Index.cshtml` file (the `Index.cshtml` file under the `Pages/Orders` f ``` -That's it! Now, you can *Build & Start* the all applications and run it in ABP Studio to see the result: +That's it! Now, you can *Start* the all applications and browse it in ABP Studio to see the result: ![web-orders-page-with-product-name](images/web-orders-page-with-product-name.png) diff --git a/docs/en/tutorials/microservice/part-07.md b/docs/en/tutorials/microservice/part-07.md index 0351df2a52..633789fc6c 100644 --- a/docs/en/tutorials/microservice/part-07.md +++ b/docs/en/tutorials/microservice/part-07.md @@ -16,7 +16,7 @@ ABP provides two types of event buses for loosely coupled communication: * [Local Event Bus](../../framework/infrastructure/event-bus/local/index.md) is suitable for in-process messaging. However, itโ€™s not suitable for microservices as it cannot communicate across different processes. For distributed systems, consider using a distributed event bus. -* **[Distributed Event Bus](../../framework/infrastructure/event-bus/distributed/index.md)** is normal for inter-process messaging, like microservices, for publishing and subscribing to distributed events. However, ABP's distributed event bus works as local (in-process) by default (actually, it uses the Local Event Bus under the hood by default) unless you configure an external message broker. +* [Distributed Event Bus](../../framework/infrastructure/event-bus/distributed/index.md) is normal for inter-process messaging, like microservices, for publishing and subscribing to distributed events. However, ABP's distributed event bus works as local (in-process) by default (actually, it uses the Local Event Bus under the hood by default) unless you configure an external message broker. In this tutorial, we will use the distributed event bus to communicate between the `Order` and `Catalog` microservices. @@ -64,7 +64,7 @@ namespace CloudCrm.OrderingService.Services; public class OrderAppService : ApplicationService, IOrderAppService { - private readonly IRepository _orderRepository; + private readonly IRepository _orderRepository; private readonly IProductIntegrationService _productIntegrationService; private readonly IDistributedEventBus _distributedEventBus; @@ -170,9 +170,9 @@ public class OrderEventHandler : IDistributedEventHandler, ITransientDependency { - private readonly IRepository _productRepository; + private readonly IProductRepository _productRepository; - public OrderEventHandler(IRepository productRepository) + public OrderEventHandler(IProductRepository productRepository) { _productRepository = productRepository; } @@ -201,7 +201,7 @@ Implementing `ITransientDependency` registers the `OrderEventHandler` class to t ### Testing the Order Creation -To keep this tutorial simple, we will not implement a user interface for creating orders. Instead, we will use the Swagger UI to create an order. Open the *Solution Runner* panel in ABP Studio and use *Build & Start* to launch the `CloudCrm.OrderingService` and `CloudCrm.CatalogService` applications. Then, go to *Run* -> *Start All* to start the remaining applications listed in the [Solution Runner root item](../../studio/running-applications.md#run). +To keep this tutorial simple, we will not implement a user interface for creating orders. Instead, we will use the Swagger UI to create an order. Open the *Solution Runner* panel in ABP Studio and use the *Start* action to launch the `CloudCrm.OrderingService` and `CloudCrm.CatalogService` applications. Then, use the *Start All* action to start the remaining applications listed in the [Solution Runner root item](../../studio/running-applications.md#run). Once the application is running and ready, [Browse](../../studio/running-applications.md#c-application) the `CloudCrm.OrderingService` application. Use the `POST /api/ordering/order` endpoint to create a new order: diff --git a/docs/en/tutorials/mobile/index.md b/docs/en/tutorials/mobile/index.md new file mode 100644 index 0000000000..51db427363 --- /dev/null +++ b/docs/en/tutorials/mobile/index.md @@ -0,0 +1,26 @@ +# Mobile Application Development Tutorial: Book Store Application + +> You must have an ABP Team or a higher license to be able to create a mobile application. + +Mobile application development tutorials are designed for developers who have completed [the web development part of the tutorial](../book-store/index.md) and wish to continue building the mobile version of the application. + +## Tutorials + +You can choose between two mobile applications: [**.NET MAUI**](../../framework/ui/maui/index.md) or [**React Native**](../../framework/ui/react-native/index.md). Choose your framework and continue building your mobile application! + +- Both guides assume you have completed the web development section of the tutorial and have the necessary backend APIs in place. +- Each guide is self-contained and provides step-by-step instructions to build the mobile app. + +### .NET MAUI + +The .NET MAUI tutorial walks you through creating a mobile version of your bookstore app using .NET technologies. + +* [.NET MAUI - Mobile Application Tutorial](./maui/index.md) +* [Source Code](https://abp.io/Account/Login?returnUrl=/api/download/samples/bookstore-maui-efcore-mobile) + +### React Native + +The React Native tutorial provides instructions for building the bookstore app using JavaScript. + +* [React Native - Mobile Application Tutorial](./react-native/index.md) +* [Source Code](https://abp.io/Account/Login?returnUrl=/api/download/samples/bookstore-react-native-mongodb) \ No newline at end of file diff --git a/docs/en/tutorials/mobile/maui/index.md b/docs/en/tutorials/mobile/maui/index.md index a9f1f92d75..c9bb520e57 100644 --- a/docs/en/tutorials/mobile/maui/index.md +++ b/docs/en/tutorials/mobile/maui/index.md @@ -2,6 +2,8 @@ ## About This Tutorial +> You must have an ABP Team or a higher license to be able to create a mobile application. + This tutorial assumes that you have completed the [Web Application Development tutorial](../../book-store/part-01.md) and built an ABP based application named `Acme.BookStore` with [MAUI](../../../get-started/maui.md) as the mobile option. Therefore, if you haven't completed the [Web Application Development tutorial](../../book-store/part-01.md), you either need to complete it or download the source code from down below and follow this tutorial. In this tutorial, we will only focus on the UI side of the `Acme.BookStore` application and we will implement the CRUD operations for a MAUI mobile application. This tutorial follows the [MVVM (Model-View-ViewModel) Pattern ](https://learn.microsoft.com/en-us/dotnet/architecture/maui/mvvm), which separates the UI from the business logic of an application. diff --git a/docs/en/tutorials/mobile/react-native/index.md b/docs/en/tutorials/mobile/react-native/index.md index 74abfa3418..2dfa3df957 100644 --- a/docs/en/tutorials/mobile/react-native/index.md +++ b/docs/en/tutorials/mobile/react-native/index.md @@ -4,6 +4,8 @@ React Native mobile option is *available for* ***Team*** *or higher licenses*. T ## About This Tutorial +> You must have an ABP Team or a higher license to be able to create a mobile application. + - This tutorial assumes that you have completed the [Web Application Development tutorial](../../book-store/part-01.md) and built an ABP based application named `Acme.BookStore` with [React Native](../../../framework/ui/react-native) as the mobile option.. Therefore, if you haven't completed the [Web Application Development tutorial](../../book-store/part-01.md), you either need to complete it or download the source code from down below and follow this tutorial. - In this tutorial, we will only focus on the UI side of the `Acme.BookStore` application and will implement the CRUD operations. - Before starting, please make sure that the [React Native Development Environment](../../../framework/ui/react-native/index.md) is ready on your machine. diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-entity-framework-core-migration.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-entity-framework-core-migration.png index 857df5f25d..ee8b847a45 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-add-entity-framework-core-migration.png and b/docs/en/tutorials/modular-crm/images/abp-studio-add-entity-framework-core-migration.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-5.png b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-5.png index 42d7c96cb2..cc3985bb75 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-5.png and b/docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-5.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-browser-orders-menu-item.png b/docs/en/tutorials/modular-crm/images/abp-studio-browser-orders-menu-item.png index 3e4d83fee1..e552239474 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-browser-orders-menu-item.png and b/docs/en/tutorials/modular-crm/images/abp-studio-browser-orders-menu-item.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-build-and-restart-application.png b/docs/en/tutorials/modular-crm/images/abp-studio-build-and-restart-application.png index 889a4251cd..bf9c9d1ead 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-build-and-restart-application.png and b/docs/en/tutorials/modular-crm/images/abp-studio-build-and-restart-application.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-entity-framework-core-update-database.png b/docs/en/tutorials/modular-crm/images/abp-studio-entity-framework-core-update-database.png index b28b4e48a0..88a0a20bb3 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-entity-framework-core-update-database.png and b/docs/en/tutorials/modular-crm/images/abp-studio-entity-framework-core-update-database.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-import-module-for-ordering-dialog.png b/docs/en/tutorials/modular-crm/images/abp-studio-import-module-for-ordering-dialog.png index 762265b2c3..2b07060c2a 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-import-module-for-ordering-dialog.png and b/docs/en/tutorials/modular-crm/images/abp-studio-import-module-for-ordering-dialog.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog.png b/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog.png index 5ea22e8e3e..8c9abf1bf7 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog.png and b/docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-module-installation-dialog.png b/docs/en/tutorials/modular-crm/images/abp-studio-module-installation-dialog.png index 9946afaaec..eb71910650 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-module-installation-dialog.png and b/docs/en/tutorials/modular-crm/images/abp-studio-module-installation-dialog.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-open-with-visual-studio-main-app.png b/docs/en/tutorials/modular-crm/images/abp-studio-open-with-visual-studio-main-app.png index f4131cfb69..62a85d6589 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-open-with-visual-studio-main-app.png and b/docs/en/tutorials/modular-crm/images/abp-studio-open-with-visual-studio-main-app.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-ordering-swagger-ui-in-browser.png b/docs/en/tutorials/modular-crm/images/abp-studio-ordering-swagger-ui-in-browser.png index 304027af9e..1318691ae9 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-ordering-swagger-ui-in-browser.png and b/docs/en/tutorials/modular-crm/images/abp-studio-ordering-swagger-ui-in-browser.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-graph-build.png b/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-graph-build.png index 10d210a0be..d88c2b80c7 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-graph-build.png and b/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-graph-build.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-initial-product-page.png b/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-initial-product-page.png index 269c55f8b4..7f9b312b23 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-initial-product-page.png and b/docs/en/tutorials/modular-crm/images/abp-studio-solution-runner-initial-product-page.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-swagger-create-order.png b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-create-order.png index e2c15fca23..01a2c5fe29 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-swagger-create-order.png and b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-create-order.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-swagger-list-orders.png b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-list-orders.png index e58ea0c51d..0fc7e63b23 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-swagger-list-orders.png and b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-list-orders.png differ diff --git a/docs/en/tutorials/modular-crm/images/abp-studio-swagger-ui-in-browser.png b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-ui-in-browser.png index 8183cde1fb..1b5fec9205 100644 Binary files a/docs/en/tutorials/modular-crm/images/abp-studio-swagger-ui-in-browser.png and b/docs/en/tutorials/modular-crm/images/abp-studio-swagger-ui-in-browser.png differ diff --git a/docs/en/tutorials/modular-crm/images/solution-explorer-modular-crm-expanded.png b/docs/en/tutorials/modular-crm/images/solution-explorer-modular-crm-expanded.png index 841b8fe610..3156b75fb1 100644 Binary files a/docs/en/tutorials/modular-crm/images/solution-explorer-modular-crm-expanded.png and b/docs/en/tutorials/modular-crm/images/solution-explorer-modular-crm-expanded.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-main-dbcontext.png b/docs/en/tutorials/modular-crm/images/visual-studio-main-dbcontext.png index 825b6c2422..0a2db8ed76 100644 Binary files a/docs/en/tutorials/modular-crm/images/visual-studio-main-dbcontext.png and b/docs/en/tutorials/modular-crm/images/visual-studio-main-dbcontext.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-new-migration-class-2.png b/docs/en/tutorials/modular-crm/images/visual-studio-new-migration-class-2.png index 3acc6e45c7..c5a698db58 100644 Binary files a/docs/en/tutorials/modular-crm/images/visual-studio-new-migration-class-2.png and b/docs/en/tutorials/modular-crm/images/visual-studio-new-migration-class-2.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-new-migration-class.png b/docs/en/tutorials/modular-crm/images/visual-studio-new-migration-class.png index b18d071cb1..24c7cb3299 100644 Binary files a/docs/en/tutorials/modular-crm/images/visual-studio-new-migration-class.png and b/docs/en/tutorials/modular-crm/images/visual-studio-new-migration-class.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-order-reporting-app-service-impl.png b/docs/en/tutorials/modular-crm/images/visual-studio-order-reporting-app-service-impl.png deleted file mode 100644 index 5007486775..0000000000 Binary files a/docs/en/tutorials/modular-crm/images/visual-studio-order-reporting-app-service-impl.png and /dev/null differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-order-reporting-app-service.png b/docs/en/tutorials/modular-crm/images/visual-studio-order-reporting-app-service.png index 7333958f11..1dcead7f0d 100644 Binary files a/docs/en/tutorials/modular-crm/images/visual-studio-order-reporting-app-service.png and b/docs/en/tutorials/modular-crm/images/visual-studio-order-reporting-app-service.png differ diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-ordering-contracts.png b/docs/en/tutorials/modular-crm/images/visual-studio-ordering-contracts.png index d9206f6630..512edaa4a1 100644 Binary files a/docs/en/tutorials/modular-crm/images/visual-studio-ordering-contracts.png and b/docs/en/tutorials/modular-crm/images/visual-studio-ordering-contracts.png differ diff --git a/docs/en/tutorials/modular-crm/part-01.md b/docs/en/tutorials/modular-crm/part-01.md index 316aff6150..ddc6e8debe 100644 --- a/docs/en/tutorials/modular-crm/part-01.md +++ b/docs/en/tutorials/modular-crm/part-01.md @@ -3,6 +3,10 @@ ````json //[doc-nav] { + "Previous": { + "Name": "Overview", + "Path": "tutorials/modular-crm/index" + }, "Next": { "Name": "Creating the initial Products module", "Path": "tutorials/modular-crm/part-02" @@ -10,7 +14,7 @@ } ```` -Follow the *[Get Started](../../get-started/layered-web-application.md)* guide to create a new layered web application with the following configuration: +Follow the *[Get Started](../../get-started/single-layer-web-application.md)* guide to create a single layer web application with the following configuration: * **Solution name**: `ModularCrm` * **UI Framework**: ASP.NET Core MVC / Razor Pages @@ -18,7 +22,7 @@ Follow the *[Get Started](../../get-started/layered-web-application.md)* guide t You can select the other options based on your preference. -> **Please complete the [Get Stared](../../get-started/layered-web-application.md) guide and run the web application before going further.** +> **Please complete the [Get Started](../../get-started/single-layer-web-application.md) guide and run the web application before going further.** The initial solution structure should be like the following in ABP Studio's *[Solution Explorer](../../studio/solution-explorer.md)*: @@ -28,10 +32,10 @@ Initially, you see a `ModularCrm` solution and a `ModularCrm` module under that > An ABP Studio module is typically a .NET solution and an ABP Studio solution is an umbrella concept for multiple .NET Solutions (see the [concepts](../../studio/concepts.md) document for more). -`ModularCrm` module is your main application, which is a layered .NET solution that consists of several packages (.NET projects). You can expand the `ModularCrm` module to see its packages: +The `ModularCrm` module is the core of your application, built as a single-layer ASP.NET Core Web application. You can expand the `ModularCrm` module to see: ![solution-explorer-modular-crm-expanded](images/solution-explorer-modular-crm-expanded.png) ## Summary -We've created the initial layered monolith solution. In the next part, we will learn how to create a new application module and install it to the main application. +We've created the initial single layer monolith solution. In the next part, we will learn how to create a new application module and install it to the main application. diff --git a/docs/en/tutorials/modular-crm/part-02.md b/docs/en/tutorials/modular-crm/part-02.md index 8a5b4b9e6c..034b4b7bfe 100644 --- a/docs/en/tutorials/modular-crm/part-02.md +++ b/docs/en/tutorials/modular-crm/part-02.md @@ -32,12 +32,13 @@ Create a `main` and a `modules` folder using the *New Folder* command, then move ## Creating The Module -There are two module templates provided by ABP Studio: +There are three module templates provided by ABP Studio: * **Empty Module**: You can use that module template to build your module structure from scratch. * **DDD Module**: A Domain Driven Design based layered module structure. +* **Standard Module**: A module template that is similar to the DDD module but without the domain layer. -We will use the *DDD Module* template for the Product module and the *Empty Module* template later in this tutorial. +We will use the *DDD Module* template for the Product module and the *Standard Module* template later in this tutorial. Right-click the `modules` folder on the *Solution Explorer* panel, and select the *Add* -> *New Module* -> *DDD Module* command: @@ -115,7 +116,7 @@ When you click the *OK* button, ABP Studio opens the *Install Module* dialog: ![abp-studio-module-installation-dialog](images/abp-studio-module-installation-dialog.png) -This dialog simplifies installing a multi-layer module to a multi-layer application. It automatically determines which package of the `ModularCrm.Products` module should be installed to which package of the main application. For example, the `ModularCrm.Products.Domain` package is installed to the `ModularCrm.Domain` package. In that way, you can use domain objects ([entities](../../framework/architecture/domain-driven-design/entities.md), [repositories](../../framework/architecture/domain-driven-design/repositories.md), ...) of the products module from the domain layer of your main application. +This dialog simplifies installing a multi-layer module to a single-layer application. It automatically determines which package of the `ModularCrm.Products` module should be installed to which package of the main application. The default package match is good for this tutorial, so you can click the *OK* button to proceed. @@ -131,7 +132,7 @@ Graph Build is a dotnet CLI command that recursively builds all the referenced d ### Run the Main Application -Open the *Solution Runner* panel, click the *Play* button (near to the solution root), right-click the `ModularCrm.Web` application and select the *Browse* command. It will open the web application in the built-in browser. Then you can navigate to the *Products* page on the main menu of the application to see the Products page that is coming from the `ModularCrm.Products` module: +Open the *Solution Runner* panel, click the *Play* button (near to the solution root), right-click the `ModularCrm` application and select the *Browse* command. It will open the web application in the built-in browser. Then you can navigate to the *Products* page on the main menu of the application to see the Products page that is coming from the `ModularCrm.Products` module: ![abp-studio-solution-runner-initial-product-page](images/abp-studio-solution-runner-initial-product-page.png) diff --git a/docs/en/tutorials/modular-crm/part-03.md b/docs/en/tutorials/modular-crm/part-03.md index 799745b9c8..3d8896d8a5 100644 --- a/docs/en/tutorials/modular-crm/part-03.md +++ b/docs/en/tutorials/modular-crm/part-03.md @@ -164,7 +164,7 @@ Open the `ModularCrm` module (which is the main application) in your IDE: ![abp-studio-open-with-visual-studio-main-app](images/abp-studio-open-with-visual-studio-main-app.png) -Find the `ModularCrmDbContext` class under the `ModularCrm.EntityFrameworkCore` project: +Open the `ModularCrmDbContext` class under the `ModularCrm` project's `Data` folder: ![visual-studio-main-dbcontext](images/visual-studio-main-dbcontext.png) @@ -185,10 +185,9 @@ Follow the three steps below; **(2)** Implement the `IProductsDbContext` by the `ModularCrmDbContext` class: ````csharp +[ReplaceDbContext(typeof(IProductsDbContext))] public class ModularCrmDbContext : AbpDbContext, - ITenantManagementDbContext, - IIdentityDbContext, IProductsDbContext //NEW: IMPLEMENT THE INTERFACE { public DbSet Products { get; set; } //NEW: ADD DBSET PROPERTY @@ -214,7 +213,7 @@ Now, we can add a new database migration. You can use Entity Framework Core's `A Ensure that the solution has built. You can right-click the `ModularCrm` (under the `main` folder) on ABP Studio *Solution Runner* and select the *Dotnet CLI* -> *Graph Build* command. -Right-click the `ModularCrm.EntityFrameworkCore` package and select the *EF Core CLI* -> *Add Migration* command: +Right-click the `ModularCrm` package and select the *EF Core CLI* -> *Add Migration* command: ![abp-studio-add-entity-framework-core-migration](images/abp-studio-add-entity-framework-core-migration.png) @@ -222,7 +221,7 @@ The *Add Migration* command opens a new dialog to get a migration name: ![abp-studio-add-entity-framework-core-migration-dialog](images/abp-studio-add-entity-framework-core-migration-dialog.png) -Once you click the *OK* button, a new database migration class is added to the `Migrations` folder of the `ModularCrm.EntityFrameworkCore` project: +Once you click the *OK* button, a new database migration class is added to the `Migrations` folder of the `ModularCrm` project: ![visual-studio-new-migration-class](images/visual-studio-new-migration-class.png) @@ -369,7 +368,7 @@ For this application, we don't need to create HTTP API endpoints for the product * You can create a regular ASP.NET Core Controller class in the `ModularCrm.Products.HttpApi` project, inject `IProductAppService` and use it to create wrapper methods. We will do this later while we create the Ordering module. * Alternatively, you can use the ABP's [Auto API Controllers](../../framework/api-development/auto-controllers.md) feature to expose your application services as API controllers by conventions. We will do it here. -Open the `ModularCrmWebModule` class in the main application's solution (the `ModularCrm` solution), find the `PreConfigureServices` method and add the following lines inside that method: +Open the `ModularCrmModule` class in the main application's solution (the `ModularCrm` solution), find the `PreConfigureServices` method and add the following lines inside that method: ````csharp PreConfigure(mvcBuilder => @@ -385,8 +384,8 @@ Then open the `ConfigureAutoApiControllers` method of the same class and add a s ````csharp Configure(options => { - options.ConventionalControllers.Create(typeof(ModularCrmApplicationModule).Assembly); - + options.ConventionalControllers.Create(typeof(ModularCrmModule).Assembly); + //ADD THE FOLLOWING LINE: options.ConventionalControllers.Create(typeof(ProductsApplicationModule).Assembly); }); @@ -404,7 +403,7 @@ This section will create a few example products using the [Swagger UI](../../fra Now, right-click the `ModularCrm` under the `main` folder in the Solution Explorer panel and select the *Dotnet CLI* -> *Graph Build* command. This will ensure that the product module and the main application are built and ready to run. -After the build process completes, open the Solution Runner panel and click the *Play* button near the solution root. Once the `ModularCrm.Web` application runs, we can right-click it and select the *Browse* command to open the user interface. +After the build process completes, open the Solution Runner panel and click the *Play* button near the solution root. Once the `ModularCrm` application runs, we can right-click it and select the *Browse* command to open the user interface. Once you see the user interface of the web application, type `/swagger` at the end of the URL to open the Swagger UI. If you scroll down, you should see the `Products` API: @@ -486,7 +485,7 @@ Here, we simply use the `IProductAppService` to get a list of all products and a ```` -You can build the product module's .NET solution (`ModularCrm.Products`), then right-click the `ModularCrm.Web` application on ABP Studio's solution runner and select the *Build & Restart* command: +Right-click the `ModularCrm` application on ABP Studio's solution runner and select the *Start* command: ![abp-studio-build-and-restart-application](images/abp-studio-build-and-restart-application.png) diff --git a/docs/en/tutorials/modular-crm/part-04.md b/docs/en/tutorials/modular-crm/part-04.md index 1674ced6c6..ba406f7846 100644 --- a/docs/en/tutorials/modular-crm/part-04.md +++ b/docs/en/tutorials/modular-crm/part-04.md @@ -70,8 +70,6 @@ Select the `ModularCrm.Ordering` module and check the *Install this module* opti ![abp-studio-install-module-dialog](images/abp-studio-install-module-dialog.png) -Select the `ModuleCrm.Ordering` package from the left area and the `ModularCrm.Domain` package from the middle area. Then, select the `ModularCrm.Ordering.UI` package from the left area and the `ModularCrm.Web` package from the middle area, as shown in the preceding figure. Finally, click *OK*. - -> Since the Ordering module is not layered, we didn't install its packages to the layers of our main application. We are installing it only to `ModularCrm.Domain`. In this way, we can use the Ordering module from any layer of our application since `ModularCrm.Domain` is one of the core packages of our application. If you build your modules as non-layered and you don't have much code in the main application's .NET solution, you can also consider creating a non-layered main application that composes these modules. +Select the `ModuleCrm.Ordering` and `ModularCrm.Ordering.UI` packages from the left area and the `ModularCrm` package from the middle area as shown in the preceding figure. Finally, click *OK*. In this part of the tutorial, we've created a standard module. This allows you to create modules or applications with a different structure. In the next part, we will add functionality to the Ordering module. diff --git a/docs/en/tutorials/modular-crm/part-05.md b/docs/en/tutorials/modular-crm/part-05.md index 91e8b2cb37..a489548b65 100644 --- a/docs/en/tutorials/modular-crm/part-05.md +++ b/docs/en/tutorials/modular-crm/part-05.md @@ -28,7 +28,7 @@ Create an `Order` class to the `ModularCrm.Ordering` project (open an `Entities` ````csharp using System; -using ModularCrm.Ordering.Contracts.Enums; +using ModularCrm.Ordering.Enums; using Volo.Abp.Domain.Entities.Auditing; namespace ModularCrm.Ordering.Entities @@ -156,7 +156,7 @@ public static class OrderingDbContextModelCreatingExtensions #### Configuring the Main Application -Open the main application's solution in your IDE, find the `ModularCrmDbContext` class under the `ModularCrm.EntityFrameworkCore` project and follow the 3 steps below: +Open the main application's solution in your IDE, find the `ModularCrmDbContext` class under the `ModularCrm` project's `Data` folder, and follow the 3 steps below: **(1)** Add the following attribute on top of the `ModularCrmDbContext` class: @@ -171,8 +171,6 @@ The `ReplaceDbContext` attribute allows the use of the `ModularCrmDbContext` cla ````csharp public class ModularCrmDbContext : AbpDbContext, - ITenantManagementDbContext, - IIdentityDbContext, IProductsDbContext, IOrderingDbContext //NEW: IMPLEMENT THE INTERFACE { @@ -200,7 +198,7 @@ Now, we can add a new database migration. You can use Entity Framework Core's `A Ensure that the solution has built. You can right-click the `ModularCrm` (under the `main` folder) on ABP Studio *Solution Runner* and select the *Dotnet CLI* -> *Graph Build* command. -Right-click the `ModularCrm.EntityFrameworkCore` package and select the *EF Core CLI* -> *Add Migration* command: +Right-click the `ModularCrm` package and select the *EF Core CLI* -> *Add Migration* command: ![abp-studio-add-entity-framework-core-migration](images/abp-studio-add-entity-framework-core-migration.png) @@ -208,11 +206,11 @@ The *Add Migration* command opens a new dialog to get a migration name: ![abp-studio-entity-framework-core-add-migration-order](images/abp-studio-entity-framework-core-add-migration-order.png) -Once you click the *OK* button, a new database migration class is added to the `Migrations` folder of the `ModularCrm.EntityFrameworkCore` project: +Once you click the *OK* button, a new database migration class is added to the `Migrations` folder of the `ModularCrm` project: ![visual-studio-new-migration-class-2](images/visual-studio-new-migration-class-2.png) -Now, you can return to ABP Studio, right-click the `ModularCrm.EntityFrameworkCore` project and select the *EF Core CLI* -> *Update Database* command: +Now, you can return to ABP Studio, right-click the `ModularCrm` project and select the *EF Core CLI* -> *Update Database* command: ![abp-studio-entity-framework-core-update-database](images/abp-studio-entity-framework-core-update-database.png) @@ -320,7 +318,7 @@ namespace ModularCrm.Ordering.Services; public class OrderAppService : OrderingAppService, IOrderAppService { - private readonly IRepository _orderRepository; + private readonly IRepository _orderRepository; public OrderAppService(IRepository orderRepository) { @@ -347,14 +345,14 @@ public class OrderAppService : OrderingAppService, IOrderAppService } ```` -Open the `ModularCrmWebModule` class in the main application's solution (the `ModularCrm` solution), find the `ConfigureAutoApiControllers` method and add the following lines inside that method: +Open the `ModularCrmModule` class in the main application's solution (the `ModularCrm` solution), find the `ConfigureAutoApiControllers` method and add the following lines inside that method: ````csharp private void ConfigureAutoApiControllers() { Configure(options => { - options.ConventionalControllers.Create(typeof(ModularCrmApplicationModule).Assembly); + options.ConventionalControllers.Create(typeof(ModularCrmModule).Assembly); options.ConventionalControllers.Create(typeof(ProductsApplicationModule).Assembly); //ADD THE FOLLOWING LINE: @@ -369,7 +367,7 @@ This section will create a few example orders using the [Swagger UI](../../frame Now, right-click the `ModularCrm` under the `main` folder in the Solution Explorer panel and select the *Dotnet CLI* -> *Graph Build* command. This will ensure that the order module and the main application are built and ready to run. -After the build process completes, open the Solution Runner panel and click the *Play* button near the solution root. Once the `ModularCrm.Web` application runs, we can right-click it and select the *Browse* command to open the user interface. +After the build process completes, open the Solution Runner panel and click the *Play* button near the solution root. Once the `ModularCrm` application runs, we can right-click it and select the *Browse* command to open the user interface. Once you see the user interface of the web application, type `/swagger` at the end of the URL to open the Swagger UI. If you scroll down, you should see the `Orders` API: @@ -487,11 +485,11 @@ public class OrderingMenuContributor : IMenuContributor ### Building the Application -Now, we will run the application to see the result. Please stop the application if it is already running. Then open the *Solution Runner* panel, right-click the `ModularCrm.Web` application, and select the *Build* -> *Graph Build* command: +Now, we will run the application to see the result. Please stop the application if it is already running. Then open the *Solution Runner* panel, right-click the `ModularCrm` application, and select the *Build* -> *Graph Build* command: ![abp-studio-solution-runner-graph-build](images/abp-studio-solution-runner-graph-build.png) -We've performed a graph build since we've made a change on a module, and more than building the main application is needed. *Graph Build* command also builds the depended modules if necessary. Alternatively, you could build the Ordering module first (on ABP Studio or your IDE), then right-click the `ModularCrm.Web` application and select the *Run* -> *Build & Start*. This approach can be faster if you have too many modules and you make a change in one of the modules. Now you can run the application by right-clicking the `ModularCrm.Web` application and selecting the *Run* -> *Start* command. +We've performed a graph build since we've made a change on a module, and more than building the main application is needed. *Graph Build* command also builds the depended modules if necessary. Alternatively, you could build the Ordering module first (on ABP Studio or your IDE). This approach can be faster if you have too many modules and you make a change in one of the modules. Now you can run the application by right-clicking the `ModularCrm` application and selecting the *Start* command. ![abp-studio-browser-orders-menu-item](images/abp-studio-browser-orders-menu-item.png) diff --git a/docs/en/tutorials/modular-crm/part-06.md b/docs/en/tutorials/modular-crm/part-06.md index 36e4f22a64..f06baa93f0 100644 --- a/docs/en/tutorials/modular-crm/part-06.md +++ b/docs/en/tutorials/modular-crm/part-06.md @@ -162,7 +162,7 @@ namespace ModularCrm.Ordering.Services; public class OrderAppService : ApplicationService, IOrderAppService { - private readonly IRepository _orderRepository; + private readonly IRepository _orderRepository; private readonly IProductIntegrationService _productIntegrationService; public OrderAppService( diff --git a/docs/en/tutorials/modular-crm/part-07.md b/docs/en/tutorials/modular-crm/part-07.md index 510e5675b3..4bcace4239 100644 --- a/docs/en/tutorials/modular-crm/part-07.md +++ b/docs/en/tutorials/modular-crm/part-07.md @@ -19,7 +19,7 @@ Another common approach to communicating between modules is messaging. By publis ABP provides two types of event buses for loosely coupled communication: * [Local Event Bus](../../framework/infrastructure/event-bus/local/index.md) is suitable for in-process messaging. Since in a modular monolith, both of publisher and subscriber are in the same process, they can communicate in-process, without needing an external message broker. -* **[Distributed Event Bus](../../framework/infrastructure/event-bus/distributed/index.md)** is normal for inter-process messaging, like microservices, for publishing and subscribing to distributed events. However, ABP's distributed event bus works as local (in-process) by default (actually, it uses the Local Event Bus under the hood by default) unless you configure an external message broker. +* [Distributed Event Bus](../../framework/infrastructure/event-bus/distributed/index.md) is normal for inter-process messaging, like microservices, for publishing and subscribing to distributed events. However, ABP's distributed event bus works as local (in-process) by default (actually, it uses the Local Event Bus under the hood by default) unless you configure an external message broker. If you consider converting your modular monolith to a microservice system later, it is best to use the Distributed Event Bus with default local/in-process implementation. It already supports database-level transactional event execution and has no performance penalty. If you switch to an external provider ([RabbitMQ](../../framework/infrastructure/event-bus/distributed/rabbitmq.md), [Kafka](../../framework/infrastructure/event-bus/distributed/kafka.md), etc.), you don't need to change your application code. @@ -74,7 +74,7 @@ namespace ModularCrm.Ordering.Services; public class OrderAppService : OrderingAppService, IOrderAppService { - private readonly IRepository _orderRepository; + private readonly IRepository _orderRepository; private readonly IProductIntegrationService _productIntegrationService; private readonly IDistributedEventBus _distributedEventBus; @@ -218,7 +218,7 @@ We inject the product repository and update the stock count in the event handler To keep this tutorial more focused, we will not create a UI for creating an order. You can easily create a form to create an order on your user interface. In this section, we will test it just using the Swagger UI. -Graph build the `ModularCrm.Web` application, run it on the ABP Studio's *Solution Runner* panel and browse the application UI as demonstrated earlier. +Graph build the `ModularCrm` application, run it on the ABP Studio's *Solution Runner* panel and browse the application UI as demonstrated earlier. Once the application is running and ready, manually type `/swagger` to the end of the URL and press the ENTER key. You should see the Swagger UI that is used to discover and test your HTTP APIs: @@ -228,8 +228,8 @@ Find the *Orders* API, click the *Try it out* button, enter a sample value the t ````json { - "productId": "0fbf7dd0-d7e9-0d18-9214-3a14d9fa1b74", - "customerName": "David" + "customerName": "David", + "productId": "e6ce1629-cfb1-1af6-e71c-3a16f10f9cc5" } ```` diff --git a/docs/en/tutorials/modular-crm/part-08.md b/docs/en/tutorials/modular-crm/part-08.md index 20efa3d7bd..6291875d5c 100644 --- a/docs/en/tutorials/modular-crm/part-08.md +++ b/docs/en/tutorials/modular-crm/part-08.md @@ -43,7 +43,7 @@ We will define the `IOrderReportingAppService` interface in the `ModularCrm.Appl As the first step, we should reference the `ModularCrm.Ordering.Contracts` package (of the `ModularCrm.Ordering` module) since we will reuse the `OrderState` enum defined in that package. -Open the ABP Studio's *Solution Explorer* panel, right-click the `ModularCrm.Application.Contracts` package and select the *Add Package Reference* command: +Open the ABP Studio's *Solution Explorer* panel, right-click the `ModularCrm` package and select the *Add Package Reference* command: ![abp-studio-add-package-reference-5](images/abp-studio-add-package-reference-5.png) @@ -55,7 +55,7 @@ The package reference has been added, and we can now use the types in the `Modul #### Defining the `IOrderReportingAppService` Interface -Open the main `ModularCrm` .NET solution in your IDE, find the `ModularCrm.Application.Contracts` project, create an `Orders` folder and add an `IOrderReportingAppService` interface. Here is the definition of that interface: +Open the main `ModularCrm` .NET solution in your IDE, create an `Orders` folder under the `Services` folder and add an `IOrderReportingAppService` interface. Here is the definition of that interface: ````csharp using System.Collections.Generic; @@ -71,7 +71,7 @@ namespace ModularCrm.Orders } ```` -We have a single method, `GetLatestOrders`, that will return a list of the latest orders. We should also define the `OrderReportDto` class that that method returns. Create the following class in the same `Orders` folder: +We have a single method, `GetLatestOrders`, that will return a list of the latest orders. We should also define the `OrderReportDto` class that that method returns. Create the `Orders` folder under the `Services/Dtos` folder and create a class named `OrderReportDto`. ````csharp using System; @@ -93,7 +93,7 @@ namespace ModularCrm.Orders } ```` -`OrderReportDto` contains data from both the `Order` and `Product` entities. We could use the `OrderState` since we have a reference to the package that defines that enum. +`OrderReportDto` contains data from both the `Order` and `Product` entities. We could use the `OrderState` since we have a reference to the package that defines that enum. After adding these files, the final folder structure should be like this: @@ -101,9 +101,7 @@ After adding these files, the final folder structure should be like this: ### Implementing the `OrderReportingAppService` Class -Create an `Orders` folder inside the `ModularCrm.Application` project and add a class named `OrderReportingAppService` inside it. The final folder structure should be like this: - -![visual-studio-order-reporting-app-service-impl](images/visual-studio-order-reporting-app-service-impl.png) +Create a class named `OrderReportingAppService` under the `Services/Orders` folder. Open the `OrderReportingAppService.cs` file and change its content by the following code block: @@ -176,7 +174,7 @@ Open the ABP Studio UI, stop the application if it is running, build and run it Here, find the `OrderReporting` API and execute it as shown above. You should get the order objects with product names. -Alternatively, you can visit the `/api/app/order-reporting/latest-orders` URL to directly execute the HTTP API on the browser (you should write the full URL, like `https://localhost:44358/api/app/order-reporting/latest-orders` - port can be different for your case) +Alternatively, you can visit the `/api/app/order-reporting/latest-orders` URL to directly execute the HTTP API on the browser (you should write the full URL, like `https://localhost:44303/api/app/order-reporting/latest-orders` - port can be different for your case) ## Summary @@ -190,7 +188,7 @@ Now, you know the fundamental principles and mechanics of building sophisticated ## Download the Source Code -You can download the completed sample solution [here](https://github.com/abpframework/abp-samples/tree/master/ModularCRM). +You can download the completed sample solution [here](https://github.com/abpframework/abp-samples/tree/master/ModularCrm). ## See Also diff --git a/docs/en/ui-themes/basic-theme/index.md b/docs/en/ui-themes/basic-theme/index.md new file mode 100644 index 0000000000..20a24dae8d --- /dev/null +++ b/docs/en/ui-themes/basic-theme/index.md @@ -0,0 +1 @@ +Read this document [here](../../framework/ui/mvc-razor-pages/basic-theme.md). diff --git a/docs/en/ui-themes/lepton-x/mvc.md b/docs/en/ui-themes/lepton-x/mvc.md index 4627ccb727..a317e8b9ed 100644 --- a/docs/en/ui-themes/lepton-x/mvc.md +++ b/docs/en/ui-themes/lepton-x/mvc.md @@ -81,6 +81,16 @@ Configure(options => > > If your layout is **TopMenu**, then you have to add them under the **wwwroot/Themes/LeptonX/Global/top-menu/css/** folder. + +#### Handling Style Changes + +You can add extra logic by using javascript API when style is changed with the following event. +```js +leptonx.CSSLoadEvent.on(event =>{ + console.log("Style is changed from " + event.detail.previousTheme + " to "+ event.detail.theme); +}); +``` + --- ### LeptonXThemeMvcOptions diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index ca1a85fd8d..c3ca5b63b2 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -467,12 +467,19 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.RemoteServices.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Abstractions", "src\Volo.Abp.AspNetCore.Abstractions\Volo.Abp.AspNetCore.Abstractions.csproj", "{E1051CD0-9262-4869-832D-B951723F4DDE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling", "src\Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling\Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling.csproj", "{2F9BA650-395C-4BE0-8CCB-9978E753562A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling", "src\Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling\Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling.csproj", "{7ADB6D92-82CC-4A2A-8BCF-FC6C6308796D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Google", "src\Volo.Abp.BlobStoring.Google\Volo.Abp.BlobStoring.Google.csproj", "{DEEB5200-BBF9-464D-9B7E-8FC035A27E94}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Google.Tests", "test\Volo.Abp.BlobStoring.Google.Tests\Volo.Abp.BlobStoring.Google.Tests.csproj", "{40FB8907-9CF7-44D0-8B5F-538AC6DAF8B9}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.ExceptionHandling.Tests", "test\Volo.Abp.ExceptionHandling.Tests\Volo.Abp.ExceptionHandling.Tests.csproj", "{E50739A7-5E2F-4EB5-AEA9-554115CB9613}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Sms.TencentCloud", "src\Volo.Abp.Sms.TencentCloud\Volo.Abp.Sms.TencentCloud.csproj", "{BE7109C5-7368-4688-8557-4A15D3F4776A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Sms.TencentCloud.Tests", "test\Volo.Abp.Sms.TencenCloud.Tests\Volo.Abp.Sms.TencentCloud.Tests.csproj", "{C753DDD6-5699-45F8-8669-08CE0BB816DE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1399,6 +1406,14 @@ Global {E1051CD0-9262-4869-832D-B951723F4DDE}.Debug|Any CPU.Build.0 = Debug|Any CPU {E1051CD0-9262-4869-832D-B951723F4DDE}.Release|Any CPU.ActiveCfg = Release|Any CPU {E1051CD0-9262-4869-832D-B951723F4DDE}.Release|Any CPU.Build.0 = Release|Any CPU + {2F9BA650-395C-4BE0-8CCB-9978E753562A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F9BA650-395C-4BE0-8CCB-9978E753562A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F9BA650-395C-4BE0-8CCB-9978E753562A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F9BA650-395C-4BE0-8CCB-9978E753562A}.Release|Any CPU.Build.0 = Release|Any CPU + {7ADB6D92-82CC-4A2A-8BCF-FC6C6308796D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7ADB6D92-82CC-4A2A-8BCF-FC6C6308796D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7ADB6D92-82CC-4A2A-8BCF-FC6C6308796D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7ADB6D92-82CC-4A2A-8BCF-FC6C6308796D}.Release|Any CPU.Build.0 = Release|Any CPU {DEEB5200-BBF9-464D-9B7E-8FC035A27E94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DEEB5200-BBF9-464D-9B7E-8FC035A27E94}.Debug|Any CPU.Build.0 = Debug|Any CPU {DEEB5200-BBF9-464D-9B7E-8FC035A27E94}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1411,6 +1426,14 @@ Global {E50739A7-5E2F-4EB5-AEA9-554115CB9613}.Debug|Any CPU.Build.0 = Debug|Any CPU {E50739A7-5E2F-4EB5-AEA9-554115CB9613}.Release|Any CPU.ActiveCfg = Release|Any CPU {E50739A7-5E2F-4EB5-AEA9-554115CB9613}.Release|Any CPU.Build.0 = Release|Any CPU + {BE7109C5-7368-4688-8557-4A15D3F4776A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE7109C5-7368-4688-8557-4A15D3F4776A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE7109C5-7368-4688-8557-4A15D3F4776A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE7109C5-7368-4688-8557-4A15D3F4776A}.Release|Any CPU.Build.0 = Release|Any CPU + {C753DDD6-5699-45F8-8669-08CE0BB816DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C753DDD6-5699-45F8-8669-08CE0BB816DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C753DDD6-5699-45F8-8669-08CE0BB816DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C753DDD6-5699-45F8-8669-08CE0BB816DE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1646,9 +1669,13 @@ Global {DFAF8763-D1D6-4EB4-B459-20E31007FE2F} = {447C8A77-E5F0-4538-8687-7383196D04EA} {DACD4485-61BE-4DE5-ACAE-4FFABC122500} = {447C8A77-E5F0-4538-8687-7383196D04EA} {E1051CD0-9262-4869-832D-B951723F4DDE} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {2F9BA650-395C-4BE0-8CCB-9978E753562A} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {7ADB6D92-82CC-4A2A-8BCF-FC6C6308796D} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {DEEB5200-BBF9-464D-9B7E-8FC035A27E94} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {40FB8907-9CF7-44D0-8B5F-538AC6DAF8B9} = {447C8A77-E5F0-4538-8687-7383196D04EA} {E50739A7-5E2F-4EB5-AEA9-554115CB9613} = {447C8A77-E5F0-4538-8687-7383196D04EA} + {BE7109C5-7368-4688-8557-4A15D3F4776A} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {C753DDD6-5699-45F8-8669-08CE0BB816DE} = {447C8A77-E5F0-4538-8687-7383196D04EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/AbpAspNetCoreComponentsMauiBlazorThemingModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/AbpAspNetCoreComponentsMauiBlazorThemingModule.cs new file mode 100644 index 0000000000..b0f6bc93f1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/AbpAspNetCoreComponentsMauiBlazorThemingModule.cs @@ -0,0 +1,34 @@ +๏ปฟusing Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling; + +[DependsOn( + typeof(AbpAspNetCoreMvcUiBundlingAbstractionsModule) +)] +public class AbpAspNetCoreComponentsMauiBlazorThemingBundlingModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.GlobalAssets.Enabled = true; + options.GlobalAssets.GlobalStyleBundleName = MauiBlazorStandardBundles.Styles.Global; + options.GlobalAssets.GlobalScriptBundleName = MauiBlazorStandardBundles.Scripts.Global; + + options + .StyleBundles + .Add(MauiBlazorStandardBundles.Styles.Global, bundle => + { + bundle.AddContributors(typeof(MauiStyleContributor)); + }); + + options + .ScriptBundles + .Add(MauiBlazorStandardBundles.Scripts.Global, bundle => + { + bundle.AddContributors(typeof(MauiScriptContributor)); + }); + }); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/FodyWeavers.xml new file mode 100644 index 0000000000..be0de3a908 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/FodyWeavers.xml @@ -0,0 +1,3 @@ +๏ปฟ + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/FodyWeavers.xsd b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/FodyWeavers.xsd @@ -0,0 +1,30 @@ +๏ปฟ + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiBlazorStandardBundles.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiBlazorStandardBundles.cs new file mode 100644 index 0000000000..645b2118c9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiBlazorStandardBundles.cs @@ -0,0 +1,14 @@ +๏ปฟnamespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling; + +public class MauiBlazorStandardBundles +{ + public static class Styles + { + public static string Global = "MauiBlazor.Global"; + } + + public static class Scripts + { + public static string Global = "MauiBlazor.Global"; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiScriptContributor.cs new file mode 100644 index 0000000000..34cda82bc8 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiScriptContributor.cs @@ -0,0 +1,13 @@ +๏ปฟusing System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling; + +public class MauiScriptContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/abp.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/lang-utils.js"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiStyleContributor.cs new file mode 100644 index 0000000000..e5ae6947de --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiStyleContributor.cs @@ -0,0 +1,18 @@ +๏ปฟusing System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling; + +public class MauiStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/libs/bootstrap/css/bootstrap.min.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/libs/fontawesome/css/all.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/css/abp.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/libs/flag-icon/css/flag-icon.css"); + context.Files.AddIfNotContains("_content/Blazorise/blazorise.css"); + context.Files.AddIfNotContains("_content/Blazorise.Bootstrap5/blazorise.bootstrap5.css"); + context.Files.AddIfNotContains("_content/Blazorise.Snackbar/blazorise.snackbar.css"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling.csproj b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling.csproj new file mode 100644 index 0000000000..e12285964f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling.csproj @@ -0,0 +1,16 @@ +๏ปฟ + + + + + + net9.0 + enable + Nullable + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/AbpAspNetCoreComponentsMauiBlazorThemingModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/AbpAspNetCoreComponentsMauiBlazorThemingModule.cs index 82a05dadb5..9ba85ebe7e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/AbpAspNetCoreComponentsMauiBlazorThemingModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/AbpAspNetCoreComponentsMauiBlazorThemingModule.cs @@ -1,9 +1,11 @@ -๏ปฟusing Volo.Abp.AspNetCore.Components.Web.Theming; +๏ปฟusing Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling; +using Volo.Abp.AspNetCore.Components.Web.Theming; using Volo.Abp.Modularity; namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming; [DependsOn( + typeof(AbpAspNetCoreComponentsMauiBlazorThemingBundlingModule), typeof(AbpAspNetCoreComponentsWebThemingModule), typeof(AbpAspNetCoreComponentsMauiBlazorModule) )] diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/ComponentsComponentsBundleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/ComponentsComponentsBundleContributor.cs index 5e03821176..6a218b08f0 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/ComponentsComponentsBundleContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/ComponentsComponentsBundleContributor.cs @@ -1,7 +1,9 @@ -๏ปฟusing Volo.Abp.Bundling; +using System; +using Volo.Abp.Bundling; namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Theming; +[Obsolete("This class is obsolete and will be removed in the future versions. Use GlobalAssets instead.")] public class ComponentsComponentsBundleContributor : IBundleContributor { public void AddScripts(BundleContext context) diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.csproj b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.csproj index d3a900f185..80322f16de 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.csproj @@ -10,6 +10,7 @@ + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/AbpAspNetCoreComponentsServerModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/AbpAspNetCoreComponentsServerModule.cs index 501fcc0b9f..59a630175f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/AbpAspNetCoreComponentsServerModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/AbpAspNetCoreComponentsServerModule.cs @@ -2,7 +2,6 @@ using System.Net; using System.Net.Http; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting.StaticWebAssets; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -31,7 +30,6 @@ public class AbpAspNetCoreComponentsServerModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - StaticWebAssetsLoader.UseStaticWebAssets(context.Services.GetHostingEnvironment(), context.Services.GetConfiguration()); context.Services.AddHttpClient(nameof(BlazorServerLookupApiRequestService)) .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpScripts.razor b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpScripts.razor index 02db9f2e30..8c9d070764 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpScripts.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpScripts.razor @@ -1,8 +1,4 @@ -@using Volo.Abp -@implements IDisposable @inject IComponentBundleManager BundleManager -@inject PersistentComponentState PersistentComponentState - @if (ScriptFiles != null) { foreach (var file in ScriptFiles) @@ -20,37 +16,18 @@ private List? ScriptFiles { get; set; } - private PersistingComponentStateSubscription persistingSubscription; - protected override async Task OnInitializedAsync() { - if (BundleName == null) - { - throw new AbpException("The BundleName parameter of the AbpScripts component can not be null!"); - } - - persistingSubscription = PersistentComponentState.RegisterOnPersisting(PersistScriptFiles); + ScriptFiles = new List(); - if (PersistentComponentState.TryTakeFromJson>(nameof(ScriptFiles), out var restoredStyleFiles)) - { - ScriptFiles = restoredStyleFiles; - } - else + if (!BundleName.IsNullOrWhiteSpace()) { ScriptFiles = (await BundleManager.GetScriptBundleFilesAsync(BundleName!)).ToList(); } - if (WebAssemblyScriptFiles != null) + if (OperatingSystem.IsBrowser() && WebAssemblyScriptFiles != null) { - ScriptFiles?.AddRange(WebAssemblyScriptFiles); + ScriptFiles.AddIfNotContains(WebAssemblyScriptFiles); } } - - private Task PersistScriptFiles() - { - PersistentComponentState.PersistAsJson(nameof(ScriptFiles), ScriptFiles); - return Task.CompletedTask; - } - - public void Dispose() => persistingSubscription.Dispose(); } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpStyles.razor b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpStyles.razor index bae9b382f3..bdcfd26bec 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpStyles.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Bundling/AbpStyles.razor @@ -1,8 +1,4 @@ -@using Volo.Abp -@implements IDisposable @inject IComponentBundleManager BundleManager -@inject PersistentComponentState PersistentComponentState - @if (StyleFiles != null) { foreach (var file in StyleFiles) @@ -20,37 +16,18 @@ private List? StyleFiles { get; set; } - private PersistingComponentStateSubscription persistingSubscription; - protected override async Task OnInitializedAsync() { - if (BundleName == null) - { - throw new AbpException("The BundleName parameter of the AbpStyles component can not be null!"); - } - - persistingSubscription = PersistentComponentState.RegisterOnPersisting(PersistStyleFiles); + StyleFiles = new List(); - if (PersistentComponentState.TryTakeFromJson>(nameof(StyleFiles), out var restoredStyleFiles)) - { - StyleFiles = restoredStyleFiles; - } - else + if (!BundleName.IsNullOrWhiteSpace()) { StyleFiles = (await BundleManager.GetStyleBundleFilesAsync(BundleName!)).ToList(); } if (OperatingSystem.IsBrowser() && WebAssemblyStyleFiles != null) { - StyleFiles?.AddRange(WebAssemblyStyleFiles); + StyleFiles.AddIfNotContains(WebAssemblyStyleFiles); } } - - private Task PersistStyleFiles() - { - PersistentComponentState.PersistAsJson(nameof(StyleFiles), StyleFiles); - return Task.CompletedTask; - } - - public void Dispose() => persistingSubscription.Dispose(); } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule.cs new file mode 100644 index 0000000000..27bfd4fa49 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule.cs @@ -0,0 +1,37 @@ +๏ปฟusing Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling; + +[DependsOn( + typeof(AbpAspNetCoreMvcUiBundlingAbstractionsModule) +)] +public class AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.GlobalAssets.Enabled = true; + options.GlobalAssets.GlobalStyleBundleName = BlazorWebAssemblyStandardBundles.Styles.Global; + options.GlobalAssets.GlobalScriptBundleName = BlazorWebAssemblyStandardBundles.Scripts.Global; + + options + .StyleBundles + .Add(BlazorWebAssemblyStandardBundles.Styles.Global, bundle => + { + bundle.AddContributors(typeof(BlazorWebAssemblyStyleContributor)); + }); + + options + .ScriptBundles + .Add(BlazorWebAssemblyStandardBundles.Scripts.Global, bundle => + { + bundle.AddContributors(typeof(BlazorWebAssemblyScriptContributor)); + }); + + options.MinificationIgnoredFiles.Add("_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"); + }); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyScriptContributor.cs new file mode 100644 index 0000000000..d24e5d6976 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyScriptContributor.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling; + +public class BlazorWebAssemblyScriptContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/abp.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/lang-utils.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/lang-utils.js"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/authentication-state-listener.js"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyStandardBundles.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyStandardBundles.cs new file mode 100644 index 0000000000..49b6ff8ba1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyStandardBundles.cs @@ -0,0 +1,14 @@ +๏ปฟnamespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling; + +public class BlazorWebAssemblyStandardBundles +{ + public static class Styles + { + public static string Global = "BlazorWebAssembly.Global"; + } + + public static class Scripts + { + public static string Global = "BlazorWebAssembly.Global"; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyStyleContributor.cs new file mode 100644 index 0000000000..b50ba58bd4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyStyleContributor.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling; + +public class BlazorWebAssemblyStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/libs/bootstrap/css/bootstrap.min.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/libs/fontawesome/css/all.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/css/abp.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/libs/flag-icon/css/flag-icon.css"); + context.Files.AddIfNotContains("_content/Blazorise/blazorise.css"); + context.Files.AddIfNotContains("_content/Blazorise.Bootstrap5/blazorise.bootstrap5.css"); + context.Files.AddIfNotContains("_content/Blazorise.Snackbar/blazorise.snackbar.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.BlazoriseUI/volo.abp.blazoriseui.css"); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/FodyWeavers.xml new file mode 100644 index 0000000000..1715698ccd --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/FodyWeavers.xsd b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/FodyWeavers.xsd new file mode 100644 index 0000000000..ffa6fc4b78 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling.csproj b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling.csproj new file mode 100644 index 0000000000..c56c744bae --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling.csproj @@ -0,0 +1,16 @@ +๏ปฟ + + + + + + net9.0 + enable + Nullable + + + + + + + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/AbpAspNetCoreComponentsWebAssemblyThemingModule.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/AbpAspNetCoreComponentsWebAssemblyThemingModule.cs index 38eff974b8..eb4e6dce44 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/AbpAspNetCoreComponentsWebAssemblyThemingModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/AbpAspNetCoreComponentsWebAssemblyThemingModule.cs @@ -1,9 +1,11 @@ ๏ปฟusing Volo.Abp.AspNetCore.Components.Web.Theming; +using Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling; using Volo.Abp.Modularity; namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming; [DependsOn( + typeof(AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule), typeof(AbpAspNetCoreComponentsWebThemingModule), typeof(AbpAspNetCoreComponentsWebAssemblyModule) )] diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs index d9b5a90e6a..816dd51889 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs @@ -1,7 +1,9 @@ -๏ปฟusing Volo.Abp.Bundling; +using System; +using Volo.Abp.Bundling; namespace Volo.Abp.AspNetCore.Components.WebAssembly.Theming; +[Obsolete("This class is obsolete and will be removed in the future versions. Use GlobalAssets instead.")] public class ComponentsComponentsBundleContributor : IBundleContributor { public void AddScripts(BundleContext context) diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj index 902163f391..ebaa3e8d43 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.csproj @@ -10,6 +10,7 @@ + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpBundlingGlobalAssetsOptions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpBundlingGlobalAssetsOptions.cs new file mode 100644 index 0000000000..206129bec0 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpBundlingGlobalAssetsOptions.cs @@ -0,0 +1,20 @@ +namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +public class AbpBundlingGlobalAssetsOptions +{ + public bool Enabled { get; set; } + + public string? GlobalStyleBundleName { get; set; } + + public string? GlobalScriptBundleName { get; set; } + + public string JavaScriptFileName { get; set; } + + public string CssFileName { get; set; } + + public AbpBundlingGlobalAssetsOptions() + { + JavaScriptFileName = "global.js"; + CssFileName = "global.css"; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpBundlingOptions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpBundlingOptions.cs index 64f63c41ec..7512bb8231 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpBundlingOptions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpBundlingOptions.cs @@ -28,6 +28,10 @@ public class AbpBundlingOptions public List PreloadStyles { get; } + public AbpBundlingGlobalAssetsOptions GlobalAssets { get; set; } + + public BundleParameterDictionary Parameters { get; set; } + public AbpBundlingOptions() { StyleBundles = new BundleConfigurationCollection(); @@ -37,5 +41,7 @@ public class AbpBundlingOptions DeferScripts = new List(); PreloadStylesByDefault = false; PreloadStyles = new List(); + GlobalAssets = new AbpBundlingGlobalAssetsOptions(); + Parameters = new BundleParameterDictionary(); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs index bc4dc14d11..af098a9b2b 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs @@ -16,11 +16,14 @@ public class BundleConfigurationContext : IBundleConfigurationContext public IAbpLazyServiceProvider LazyServiceProvider { get; } - public BundleConfigurationContext(IServiceProvider serviceProvider, IFileProvider fileProvider) + public BundleParameterDictionary Parameters { get; set; } + + public BundleConfigurationContext(IServiceProvider serviceProvider, IFileProvider fileProvider, BundleParameterDictionary? parameters = null) { Files = new List(); ServiceProvider = serviceProvider; LazyServiceProvider = ServiceProvider.GetRequiredService(); FileProvider = fileProvider; + Parameters = parameters ?? new BundleParameterDictionary(); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleParameterDictionary.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleParameterDictionary.cs new file mode 100644 index 0000000000..df4dc573a4 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleParameterDictionary.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +public class BundleParameterDictionary : Dictionary +{ + public const string InteractiveAutoPropertyName = "InteractiveAuto"; + + public bool InteractiveAuto + { + get + { + return TryGetValue(InteractiveAutoPropertyName, out var value) && bool.Parse(value); + } + set + { + this[InteractiveAutoPropertyName] = value.ToString(); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpAspNetCoreMvcUiBundlingModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpAspNetCoreMvcUiBundlingModule.cs index f0f7f94439..2292c93a53 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpAspNetCoreMvcUiBundlingModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/AbpAspNetCoreMvcUiBundlingModule.cs @@ -1,8 +1,21 @@ -๏ปฟusing Volo.Abp.AspNetCore.Mvc.Libs; +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap; +using Volo.Abp.AspNetCore.VirtualFileSystem; +using Volo.Abp.Bundling.Styles; +using Volo.Abp.AspNetCore.Mvc.Libs; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap; using Volo.Abp.Data; using Volo.Abp.Minify; using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; @@ -23,4 +36,96 @@ public class AbpAspNetCoreMvcUiBundlingModule : AbpModule }); } } + + public async override Task OnApplicationInitializationAsync(ApplicationInitializationContext context) + { + var environment = context.GetEnvironmentOrNull(); + if (environment != null) + { + environment.WebRootFileProvider = + new CompositeFileProvider( + context.GetEnvironment().WebRootFileProvider, + context.ServiceProvider.GetRequiredService() + ); + } + + await InitialGlobalAssetsAsync(context); + } + + protected virtual async Task InitialGlobalAssetsAsync(ApplicationInitializationContext context) + { + var bundlingOptions = context.ServiceProvider.GetRequiredService>().Value; + var logger = context.ServiceProvider.GetRequiredService>(); + if (!bundlingOptions.GlobalAssets.Enabled) + { + return; + } + + var bundleManager = context.ServiceProvider.GetRequiredService(); + var webHostEnvironment = context.ServiceProvider.GetRequiredService(); + var dynamicFileProvider = context.ServiceProvider.GetRequiredService(); + if (!bundlingOptions.GlobalAssets.GlobalStyleBundleName.IsNullOrWhiteSpace()) + { + var styleFiles = await bundleManager.GetStyleBundleFilesAsync(bundlingOptions.GlobalAssets.GlobalStyleBundleName); + var styles = string.Empty; + foreach (var file in styleFiles) + { + var fileInfo = webHostEnvironment.WebRootFileProvider?.GetFileInfo(file.FileName); + if (fileInfo == null || !fileInfo.Exists) + { + logger.LogError($"Could not find the file: {file.FileName}"); + continue; + } + + var fileContent = await fileInfo.ReadAsStringAsync(); + if (!bundleManager.IsBundlingEnabled()) + { + fileContent = CssRelativePath.Adjust(fileContent, + file.FileName, + Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")); + + styles += $"/*{file.FileName}*/{Environment.NewLine}{fileContent}{Environment.NewLine}{Environment.NewLine}"; + } + else + { + styles += $"{fileContent}{Environment.NewLine}{Environment.NewLine}"; + } + } + + dynamicFileProvider.AddOrUpdate( + new InMemoryFileInfo("/wwwroot/" + bundlingOptions.GlobalAssets.CssFileName, + Encoding.UTF8.GetBytes(styles), + bundlingOptions.GlobalAssets.CssFileName)); + } + + if (!bundlingOptions.GlobalAssets.GlobalScriptBundleName.IsNullOrWhiteSpace()) + { + var scriptFiles = await bundleManager.GetScriptBundleFilesAsync(bundlingOptions.GlobalAssets.GlobalScriptBundleName); + var scripts = string.Empty; + foreach (var file in scriptFiles) + { + var fileInfo = webHostEnvironment.WebRootFileProvider?.GetFileInfo(file.FileName); + if (fileInfo == null || !fileInfo.Exists) + { + logger.LogError($"Could not find the file: {file.FileName}"); + continue; + } + + var fileContent = await fileInfo.ReadAsStringAsync(); + if (!bundleManager.IsBundlingEnabled()) + { + scripts += $"{fileContent.EnsureEndsWith(';')}{Environment.NewLine}{Environment.NewLine}"; + } + else + { + scripts += $"//{file.FileName}{Environment.NewLine}{fileContent.EnsureEndsWith(';')}{Environment.NewLine}{Environment.NewLine}"; + } + } + + dynamicFileProvider.AddOrUpdate( + new InMemoryFileInfo("/wwwroot/" + bundlingOptions.GlobalAssets.JavaScriptFileName, + Encoding.UTF8.GetBytes(scripts), + bundlingOptions.GlobalAssets.JavaScriptFileName)); + } + } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs index 20c33e86e5..9ac43585c0 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs @@ -172,7 +172,7 @@ public class BundleManager : IBundleManager, ITransientDependency ); } - protected virtual bool IsBundlingEnabled() + public virtual bool IsBundlingEnabled() { switch (Options.Mode) { @@ -240,7 +240,7 @@ public class BundleManager : IBundleManager, ITransientDependency protected virtual BundleConfigurationContext CreateBundleConfigurationContext() { - return new BundleConfigurationContext(ServiceProvider, HostingEnvironment.WebRootFileProvider); + return new BundleConfigurationContext(ServiceProvider, HostingEnvironment.WebRootFileProvider, Options.Parameters); } protected virtual List GetContributors(BundleConfigurationCollection bundles, string bundleName) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/Styles/StyleBundler.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/Styles/StyleBundler.cs index 6623a2d667..b20e9af7ee 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/Styles/StyleBundler.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/Styles/StyleBundler.cs @@ -2,6 +2,8 @@ using System; using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.VirtualFileSystem; +using Volo.Abp.Bundling.Styles; using Volo.Abp.Minify.Styles; namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.Styles; diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js index 0f2b028daf..e3e1f8fe6c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js @@ -87,7 +87,7 @@ var abp = abp || {}; if (field.items.length === 1) { var firstItem = field.items[0]; if (!getVisibilityValue(firstItem.visible, record, tableInstance)) { - return ""; + return $(''); } var $button = $(''); @@ -181,7 +181,7 @@ var abp = abp || {}; if ($dropdownItemsContainer.find('li').length > 0) { $dropdownItemsContainer.appendTo($container); } else { - $dropdownButton.attr('disabled', 'disabled'); + $dropdownButton.addClass('d-none'); } $dropdownButton.prependTo($container); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery/jquery-extensions.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery/jquery-extensions.js index bae3e19c93..5928393329 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery/jquery-extensions.js +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery/jquery-extensions.js @@ -161,9 +161,17 @@ $.fn.needConfirmationOnUnsavedClose = function ($modal) { var $form = $(this); var formSaved = false; - var unEditedForm = JSON.stringify($form.serializeFormToObject()); + var unEditedForm; + + $modal.on("shown.bs.modal", function () { + unEditedForm = JSON.stringify($form.serializeFormToObject()); + }); $modal.on("hide.bs.modal", function (e) { + if(unEditedForm === undefined) { + return; + } + var currentForm = JSON.stringify($form.serializeFormToObject()); var thereAreUnsavedChanges = currentForm !== unEditedForm; diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs index 10900f6ee4..097fd899a5 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs @@ -242,13 +242,19 @@ public class CachedObjectExtensionsDtoService : ICachedObjectExtensionsDtoServic e => e.GetProperties() ) ) - .Where(p => p.Type.IsEnum) + .Where(p => p.Type.IsEnum || TypeHelper.IsNullableEnum(p.Type)) .ToList(); foreach (var enumProperty in enumProperties) { - // ReSharper disable once AssignNullToNotNullAttribute (enumProperty.Type.FullName can not be null for this case) - objectExtensionsDto.Enums[enumProperty.Type.FullName!] = CreateExtensionEnumDto(enumProperty); + if (TypeHelper.IsNullableEnum(enumProperty.Type)) + { + objectExtensionsDto.Enums[Nullable.GetUnderlyingType(enumProperty.Type)!.FullName + "?"] = CreateExtensionEnumDto(enumProperty); + } + else + { + objectExtensionsDto.Enums[enumProperty.Type.FullName!] = CreateExtensionEnumDto(enumProperty); + } } } @@ -260,12 +266,23 @@ public class CachedObjectExtensionsDtoService : ICachedObjectExtensionsDtoServic LocalizationResource = enumProperty.GetLocalizationResourceNameOrNull() }; - foreach (var enumValue in enumProperty.Type.GetEnumValues()) + var enumType = enumProperty.Type.IsEnum + ? enumProperty.Type + : TypeHelper.IsNullableEnum(enumProperty.Type) + ? Nullable.GetUnderlyingType(enumProperty.Type) + : null; + + if (enumType == null) + { + return extensionEnumDto; + } + + foreach (var enumValue in enumType.GetEnumValues()) { extensionEnumDto.Fields.Add( new ExtensionEnumFieldDto { - Name = enumProperty.Type.GetEnumName(enumValue)!, + Name = enumType.GetEnumName(enumValue)!, Value = enumValue } ); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsErrorPage.Designer.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsErrorPage.Designer.cs index 9d30ea018c..4fe8e51000 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsErrorPage.Designer.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsErrorPage.Designer.cs @@ -27,20 +27,100 @@ using Volo.Abp.AspNetCore.RazorViews; #line hidden #nullable disable WriteLiteral(@" - - - - Error - The Libs folder is missing! - - -

⚠️ The Libs folder under the wwwroot/libs directory is empty!

- + + + + + + Error - The Libs Folder is Missing! + + + +
+

⚠️ The Libs folder is missing!

+
+

The Libs folder contains mandatory NPM Packages for running the project.

- -

Make sure you run the abp install-libs CLI tool command.

- -

For more information, check out the ABP CLI documentation

- +

Make sure you run the abp install-lib"); + WriteLiteral(@"s CLI tool command.

+

+ If your application does not use any client-side libraries, you can disable this check by setting + AbpMvcLibsOptions.CheckLibs to false, as shown below: +

+
+Configure<AbpMvcLibsOptions>(options =>
+{
+    options.CheckLibs = false;
+});
+

For more information, check out the ABP CLI documentation.

+
+
+ © ABP Framework +
+ "); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsErrorPage.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsErrorPage.cshtml index 5a5d6c8d19..7b15ce94bf 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsErrorPage.cshtml +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsErrorPage.cshtml @@ -5,18 +5,96 @@ Response.StatusCode = 500; } - - - - Error - The Libs folder is missing! - - -

⚠️ The Libs folder under the wwwroot/libs directory is empty!

- + + + + + + Error - The Libs Folder is Missing! + + + +
+

⚠️ The Libs folder is missing!

+
+

The Libs folder contains mandatory NPM Packages for running the project.

- -

Make sure you run the abp install-libs CLI tool command.

- -

For more information, check out the ABP CLI documentation

- +

Make sure you run the abp install-libs CLI tool command.

+

+ If your application does not use any client-side libraries, you can disable this check by setting + AbpMvcLibsOptions.CheckLibs to false, as shown below: +

+
+Configure<AbpMvcLibsOptions>(options =>
+{
+    options.CheckLibs = false;
+});
+

For more information, check out the ABP CLI documentation.

+
+
+ © ABP Framework +
+ diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsService.cs index 410260674b..dc6faf723c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsService.cs @@ -64,7 +64,7 @@ public class AbpMvcLibsService : IAbpMvcLibsService, ITransientDependency var webHostEnvironment = httpContext.RequestServices.GetRequiredService(); if (webHostEnvironment.WebRootPath.IsNullOrWhiteSpace()) { - logger.LogWarning("The 'WebRootPath' is not set! The 'CheckLibs' feature is disabled!"); + logger.LogInformation("The 'WebRootPath' is not set, The 'CheckLibs' feature not needed."); return Task.FromResult(true); } @@ -72,7 +72,8 @@ public class AbpMvcLibsService : IAbpMvcLibsService, ITransientDependency var libsFolder = fileProvider.GetDirectoryContents("/libs"); if (!libsFolder.Exists || !libsFolder.Any()) { - logger.LogError("The 'wwwroot/libs' folder does not exist or empty!"); + logger.LogError("The 'wwwroot/libs' folder does not exist or empty! " + + "If your application does not use any client-side libraries, you can disable this check by setting 'AbpMvcLibsOptions.CheckLibs' to 'false'"); return Task.FromResult(false); } } diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs index 3cada3bf46..1bf3e45e4a 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs @@ -177,14 +177,14 @@ public static class AbpApplicationBuilderExtensions throw new AbpException("The app(IApplicationBuilder) is not an IEndpointRouteBuilder."); } - app.UseVirtualStaticFiles(); - var options = app.ApplicationServices.GetRequiredService>().Value; foreach (var folder in options.AllowedExtraWebContentFolders) { app.UseVirtualStaticFiles(folder); } + app.UseVirtualStaticFiles(); + return endpoints.MapStaticAssets(staticAssetsManifestPath); } diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs index 35d30e9f64..bd767f93fb 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/DefaultAbpRequestLocalizationOptionsProvider.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using DeviceDetectorNET; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.DependencyInjection; @@ -57,7 +58,7 @@ public class DefaultAbpRequestLocalizationOptionsProvider : ? new RequestLocalizationOptions() : new RequestLocalizationOptions { - DefaultRequestCulture = DefaultGetRequestCulture(defaultLanguage, languages), + DefaultRequestCulture = GetDefaultRequestCulture(defaultLanguage, languages), SupportedCultures = languages .Select(l => l.CultureName) .Distinct() @@ -87,15 +88,22 @@ public class DefaultAbpRequestLocalizationOptionsProvider : return _requestLocalizationOptions; } - private static RequestCulture DefaultGetRequestCulture(string? defaultLanguage, IReadOnlyList languages) + private static RequestCulture GetDefaultRequestCulture(string? defaultLanguage, IReadOnlyList languages) { if (defaultLanguage == null) { - var firstLanguage = languages.First(); + var firstLanguage = languages.FirstOrDefault() ?? new LanguageInfo("en", "en"); return new RequestCulture(firstLanguage.CultureName, firstLanguage.UiCultureName); } var (cultureName, uiCultureName) = LocalizationSettingHelper.ParseLanguageSetting(defaultLanguage); + + if (languages.Any() && languages.All(l => l.CultureName != cultureName)) + { + var firstLanguage = languages.First(); + return new RequestCulture(firstLanguage.CultureName, firstLanguage.UiCultureName); + } + return new RequestCulture(cultureName, uiCultureName); } @@ -106,4 +114,4 @@ public class DefaultAbpRequestLocalizationOptionsProvider : _requestLocalizationOptions = null; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/AbpAspNetCoreModule.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/AbpAspNetCoreModule.cs index cda6aea27f..64fab10ac6 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/AbpAspNetCoreModule.cs +++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/AbpAspNetCoreModule.cs @@ -1,5 +1,6 @@ ๏ปฟusing System; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.StaticWebAssets; using Microsoft.AspNetCore.RequestLocalization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; @@ -56,6 +57,8 @@ public class AbpAspNetCoreModule : AbpModule AddAspNetServices(context.Services); context.Services.AddObjectAccessor(); context.Services.AddAbpDynamicOptions(); + + StaticWebAssetsLoader.UseStaticWebAssets(context.Services.GetHostingEnvironment(), context.Services.GetConfiguration()); } private static void AddAspNetServices(IServiceCollection services) diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersMiddleware.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersMiddleware.cs index 4542dd32e1..2d231c46ee 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersMiddleware.cs +++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersMiddleware.cs @@ -23,6 +23,14 @@ public class AbpSecurityHeadersMiddleware : AbpMiddlewareBase, ITransientDepende public async override Task InvokeAsync(HttpContext context, RequestDelegate next) { + var endpoint = context.GetEndpoint(); + + if (endpoint?.Metadata.GetMetadata() != null) + { + await next.Invoke(context); + return; + } + /*X-Content-Type-Options header tells the browser to not try and โ€œguessโ€ what a mimetype of a resource might be, and to just take what mimetype the server has returned as fact.*/ AddHeader(context, "X-Content-Type-Options", "nosniff"); @@ -35,14 +43,6 @@ public class AbpSecurityHeadersMiddleware : AbpMiddlewareBase, ITransientDepende var requestAcceptTypeHtml = context.Request.Headers["Accept"].Any(x => x!.Contains("text/html") || x.Contains("*/*") || x.Contains("application/xhtml+xml")); - var endpoint = context.GetEndpoint(); - - if (endpoint?.Metadata.GetMetadata() != null) - { - await next.Invoke(context); - return; - } - if (!requestAcceptTypeHtml || !Options.Value.UseContentSecurityPolicyHeader || await AlwaysIgnoreContentTypes(context) diff --git a/framework/src/Volo.Abp.Authorization/Microsoft/AspNetCore/Authorization/AbpAuthorizationServiceExtensions.cs b/framework/src/Volo.Abp.Authorization/Microsoft/AspNetCore/Authorization/AbpAuthorizationServiceExtensions.cs index c66d74b381..3dfc5218d0 100644 --- a/framework/src/Volo.Abp.Authorization/Microsoft/AspNetCore/Authorization/AbpAuthorizationServiceExtensions.cs +++ b/framework/src/Volo.Abp.Authorization/Microsoft/AspNetCore/Authorization/AbpAuthorizationServiceExtensions.cs @@ -108,6 +108,11 @@ public static class AbpAuthorizationServiceExtensions return (await authorizationService.AuthorizeAsync(resource, policyName)).Succeeded; } + /// + /// Checks if CurrentPrincipal meets a specific authorization policy, throwing an if not. + /// + /// The providing authorization. + /// The name of the policy to evaluate. public static async Task CheckAsync(this IAuthorizationService authorizationService, string policyName) { if (!await authorizationService.IsGrantedAsync(policyName)) @@ -117,6 +122,12 @@ public static class AbpAuthorizationServiceExtensions } } + /// + /// Checks if CurrentPrincipal meets a specific requirement for the specified resource, throwing an if not. + /// + /// The providing authorization. + /// The resource to evaluate the policy against. + /// The requirement to evaluate the policy against. public static async Task CheckAsync(this IAuthorizationService authorizationService, object resource, IAuthorizationRequirement requirement) { if (!await authorizationService.IsGrantedAsync(resource, requirement)) @@ -126,6 +137,12 @@ public static class AbpAuthorizationServiceExtensions } } + /// + /// Checks if CurrentPrincipal meets a specific authorization policy against the specified resource, throwing an if not. + /// + /// The providing authorization. + /// The resource to evaluate the policy against. + /// The policy to evaluate. public static async Task CheckAsync(this IAuthorizationService authorizationService, object resource, AuthorizationPolicy policy) { if (!await authorizationService.IsGrantedAsync(resource, policy)) @@ -135,6 +152,11 @@ public static class AbpAuthorizationServiceExtensions } } + /// + /// Checks if CurrentPrincipal meets a specific authorization policy, throwing an if not. + /// + /// The providing authorization. + /// The policy to evaluate. public static async Task CheckAsync(this IAuthorizationService authorizationService, AuthorizationPolicy policy) { if (!await authorizationService.IsGrantedAsync(policy)) @@ -143,6 +165,12 @@ public static class AbpAuthorizationServiceExtensions } } + /// + /// Checks if CurrentPrincipal meets a specific authorization policy against the specified resource, throwing an if not. + /// + /// The providing authorization. + /// The resource to evaluate the policy against. + /// The requirements to evaluate the policy against. public static async Task CheckAsync(this IAuthorizationService authorizationService, object resource, IEnumerable requirements) { if (!await authorizationService.IsGrantedAsync(resource, requirements)) @@ -152,6 +180,12 @@ public static class AbpAuthorizationServiceExtensions } } + /// + /// Checks if CurrentPrincipal meets a specific authorization policy against the specified resource, throwing an if not. + /// + /// The providing authorization. + /// The resource to evaluate the policy against. + /// The name of the policy to evaluate. public static async Task CheckAsync(this IAuthorizationService authorizationService, object resource, string policyName) { if (!await authorizationService.IsGrantedAsync(resource, policyName)) diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerOptions.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerOptions.cs index 6fc0daca56..3bc31e7a39 100644 --- a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerOptions.cs +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerOptions.cs @@ -33,6 +33,12 @@ public class AbpBackgroundJobWorkerOptions /// public double DefaultWaitFactor { get; set; } + /// + /// Distributed lock name for the worker. + /// Default value: "AbpBackgroundJobWorker". + /// + public string DistributedLockName { get; set; } + public AbpBackgroundJobWorkerOptions() { MaxJobFetchCount = 1000; @@ -40,5 +46,6 @@ public class AbpBackgroundJobWorkerOptions DefaultFirstWaitDuration = 60; DefaultTimeout = 172800; DefaultWaitFactor = 2.0; + DistributedLockName = "AbpBackgroundJobWorker"; } } diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs index 4aeaf59885..4313350098 100644 --- a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs @@ -13,8 +13,6 @@ namespace Volo.Abp.BackgroundJobs; public class BackgroundJobWorker : AsyncPeriodicBackgroundWorkerBase, IBackgroundJobWorker { - protected const string DistributedLockName = "AbpBackgroundJobWorker"; - protected AbpBackgroundJobOptions JobOptions { get; } protected AbpBackgroundJobWorkerOptions WorkerOptions { get; } @@ -39,7 +37,7 @@ public class BackgroundJobWorker : AsyncPeriodicBackgroundWorkerBase, IBackgroun protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) { - await using (var handler = await DistributedLock.TryAcquireAsync(DistributedLockName, cancellationToken: StoppingToken)) + await using (var handler = await DistributedLock.TryAcquireAsync(WorkerOptions.DistributedLockName, cancellationToken: StoppingToken)) { if (handler != null) { diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor b/framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor index 29010dded6..2547a7930c 100644 --- a/framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor +++ b/framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor @@ -112,7 +112,7 @@ Sortable="@column.Sortable" Displayable="column.Visible"> - @(GetConvertedFieldValue(context, column)) + @((MarkupString)GetConvertedFieldValue(context, column))
} @@ -140,7 +140,7 @@ { if (column.ValueConverter != null) { - @(GetConvertedFieldValue(context, column)) + @((MarkupString)GetConvertedFieldValue(context, column)) } else { diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/Styles/CssRelativePathAdjuster.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/Styles/CssRelativePathAdjuster.cs deleted file mode 100644 index c1cb76aa22..0000000000 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/Styles/CssRelativePathAdjuster.cs +++ /dev/null @@ -1,76 +0,0 @@ -๏ปฟusing System; -using System.IO; -using System.Text.RegularExpressions; - -namespace Volo.Abp.Cli.Bundling.Styles; - -internal static class CssRelativePathAdjuster -{ - private static readonly Regex _rxUrl = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - public static string Adjust( - string cssFileContents, - string absoluteInputFilePath, - string absoluteOutputPath) - { - var matches = _rxUrl.Matches(cssFileContents); - - if (matches.Count <= 0) - { - return cssFileContents; - } - - var cssDirectoryPath = Path.GetDirectoryName(absoluteInputFilePath); - - foreach (Match match in matches) - { - string quoteDelimiter = match.Groups[1].Value; //url('') vs url("") - string relativePathToCss = match.Groups[2].Value; - - // Ignore root relative references - if (relativePathToCss.StartsWith("/", StringComparison.Ordinal)) - continue; - - //prevent query string from causing error - var pathAndQuery = relativePathToCss.Split(new[] { '?' }, 2, StringSplitOptions.RemoveEmptyEntries); - var pathOnly = pathAndQuery[0]; - var queryOnly = pathAndQuery.Length == 2 ? pathAndQuery[1] : string.Empty; - - string absolutePath = GetAbsolutePath(cssDirectoryPath, pathOnly); - string serverRelativeUrl = MakeRelative(absoluteOutputPath, absolutePath); - - if (!string.IsNullOrEmpty(queryOnly)) - serverRelativeUrl += "?" + queryOnly; - - string replace = string.Format("url({0}{1}{0})", quoteDelimiter, serverRelativeUrl); - - cssFileContents = cssFileContents.Replace(match.Groups[0].Value, replace); - } - - return cssFileContents; - } - - private static string GetAbsolutePath(string cssFilePath, string pathOnly) - { - return Path.GetFullPath(Path.Combine(cssFilePath, pathOnly)); - } - - private static readonly string _protocol = "file:///"; - private static string MakeRelative(string baseFile, string file) - { - if (string.IsNullOrEmpty(file)) - return file; - - Uri baseUri = new Uri(_protocol + baseFile, UriKind.RelativeOrAbsolute); - Uri fileUri = new Uri(_protocol + file, UriKind.RelativeOrAbsolute); - - if (baseUri.IsAbsoluteUri) - { - return Uri.UnescapeDataString(baseUri.MakeRelativeUri(fileUri).ToString()); - } - else - { - return baseUri.ToString(); - } - } -} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/Styles/StyleBundler.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/Styles/StyleBundler.cs index 0e46b91060..552c815ced 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/Styles/StyleBundler.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/Styles/StyleBundler.cs @@ -2,6 +2,7 @@ using System.IO; using System.Text; using Volo.Abp.Bundling; +using Volo.Abp.Bundling.Styles; using Volo.Abp.DependencyInjection; using Volo.Abp.Minify.Styles; @@ -44,7 +45,7 @@ public class StyleBundler : BundlerBase, IStyleBundler, ITransientDependency protected override string ProcessBeforeAddingToTheBundle(string referencePath, string bundleDirectory, string fileContent) { - return CssRelativePathAdjuster.Adjust( + return CssRelativePath.Adjust( fileContent, referencePath, bundleDirectory diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs index d3a7b92a34..365ea81431 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs @@ -72,7 +72,7 @@ public class InstallLibsService : IInstallLibsService, ITransientDependency } else { - NpmHelper.RunNpmInstall(projectDirectory, "--no-audit"); + NpmHelper.RunNpmInstall(projectDirectory, "--legacy-peer-deps"); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/Styles/CssRelativePath.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Bundling/Styles/CssRelativePath.cs similarity index 57% rename from framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/Styles/CssRelativePath.cs rename to framework/src/Volo.Abp.Core/Volo/Abp/Bundling/Styles/CssRelativePath.cs index c41f0ca4a4..4ee53124b0 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/Styles/CssRelativePath.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Bundling/Styles/CssRelativePath.cs @@ -2,18 +2,18 @@ using System.IO; using System.Text.RegularExpressions; -namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.Styles; +namespace Volo.Abp.Bundling.Styles; -internal static class CssRelativePath +public static class CssRelativePath { - private static readonly Regex _rxUrl = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private readonly static Regex RxUrl = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); public static string Adjust( string cssFileContents, string absoluteInputFilePath, string absoluteOutputPath) { - var matches = _rxUrl.Matches(cssFileContents); + var matches = RxUrl.Matches(cssFileContents); if (matches.Count <= 0) { @@ -24,25 +24,29 @@ internal static class CssRelativePath foreach (Match match in matches) { - string quoteDelimiter = match.Groups[1].Value; //url('') vs url("") - string relativePathToCss = match.Groups[2].Value; + var quoteDelimiter = match.Groups[1].Value; //url('') vs url("") + var relativePathToCss = match.Groups[2].Value; // Ignore root relative references if (relativePathToCss.StartsWith("/", StringComparison.Ordinal)) + { continue; + } //prevent query string from causing error var pathAndQuery = relativePathToCss.Split(new[] { '?' }, 2, StringSplitOptions.RemoveEmptyEntries); var pathOnly = pathAndQuery[0]; var queryOnly = pathAndQuery.Length == 2 ? pathAndQuery[1] : string.Empty; - string absolutePath = GetAbsolutePath(cssDirectoryPath, pathOnly); - string serverRelativeUrl = MakeRelative(absoluteOutputPath, absolutePath); + var absolutePath = GetAbsolutePath(cssDirectoryPath, pathOnly); + var serverRelativeUrl = MakeRelative(absoluteOutputPath, absolutePath); if (!string.IsNullOrEmpty(queryOnly)) + { serverRelativeUrl += "?" + queryOnly; + } - string replace = string.Format("url({0}{1}{0})", quoteDelimiter, serverRelativeUrl); + var replace = string.Format("url({0}{1}{0})", quoteDelimiter, serverRelativeUrl); cssFileContents = cssFileContents.Replace(match.Groups[0].Value, replace); } @@ -55,22 +59,18 @@ internal static class CssRelativePath return Path.GetFullPath(Path.Combine(cssFilePath, pathOnly)); } - private static readonly string _protocol = "file:///"; + private const string Protocol = "file:///"; + private static string MakeRelative(string baseFile, string file) { if (string.IsNullOrEmpty(file)) + { return file; + } - Uri baseUri = new Uri(_protocol + baseFile, UriKind.RelativeOrAbsolute); - Uri fileUri = new Uri(_protocol + file, UriKind.RelativeOrAbsolute); + var baseUri = new Uri(Protocol + baseFile, UriKind.RelativeOrAbsolute); + var fileUri = new Uri(Protocol + file, UriKind.RelativeOrAbsolute); - if (baseUri.IsAbsoluteUri) - { - return Uri.UnescapeDataString(baseUri.MakeRelativeUri(fileUri).ToString()); - } - else - { - return baseUri.ToString(); - } + return baseUri.IsAbsoluteUri ? Uri.UnescapeDataString(baseUri.MakeRelativeUri(fileUri).ToString()) : baseUri.ToString(); } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs index 1efa6c4f29..f9fdea3e50 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs @@ -84,6 +84,14 @@ public static class TypeHelper return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); } + public static bool IsNullableEnum(Type type) + { + return type.IsGenericType && + type.GetGenericTypeDefinition() == typeof(Nullable<>) && + type.GenericTypeArguments.Length == 1 && + type.GenericTypeArguments[0].IsEnum; + } + public static Type GetFirstGenericArgumentIfNullable(this Type t) { if (t.GetGenericArguments().Length > 0 && t.GetGenericTypeDefinition() == typeof(Nullable<>)) @@ -309,6 +317,10 @@ public static class TypeHelper { return "object"; } + else if (type.IsEnum) + { + return "enum"; + } return type.FullName ?? type.Name; } diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/EmailSenderBase.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/EmailSenderBase.cs index dacbb195ab..4f07c8a1c5 100644 --- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/EmailSenderBase.cs +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/EmailSenderBase.cs @@ -92,7 +92,7 @@ public abstract class EmailSenderBase : IEmailSender public virtual async Task QueueAsync(string to, string subject, string body, bool isBodyHtml = true, AdditionalEmailSendingArgs? additionalEmailSendingArgs = null) { - ValidateEmailAddress(to); + await ValidateEmailAddressAsync(to); if (!BackgroundJobManager.IsAvailable()) { @@ -115,7 +115,7 @@ public abstract class EmailSenderBase : IEmailSender public virtual async Task QueueAsync(string from, string to, string subject, string body, bool isBodyHtml = true, AdditionalEmailSendingArgs? additionalEmailSendingArgs = null) { - ValidateEmailAddress(to); + await ValidateEmailAddressAsync(to); if (!BackgroundJobManager.IsAvailable()) { @@ -176,13 +176,16 @@ public abstract class EmailSenderBase : IEmailSender } } - private static void ValidateEmailAddress(string emailAddress) + protected virtual Task ValidateEmailAddressAsync(string emailAddress) { - if(ValidationHelper.IsValidEmailAddress(emailAddress)) + try { - return; + _ = new MailAddressCollection { emailAddress }; + return Task.CompletedTask; + } + catch (Exception e) + { + throw new ArgumentException($"Email address '{emailAddress}' is not valid!"); } - - throw new ArgumentException($"Email address '{emailAddress}' is not valid!"); } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventInbox.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventInbox.cs index f5953ea3b1..54ddfd0e5e 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventInbox.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventInbox.cs @@ -1,6 +1,7 @@ ๏ปฟusing System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -36,14 +37,21 @@ public class DbContextEventInbox : IDbContextEventInbox } [UnitOfWork] - public virtual async Task> GetWaitingEventsAsync(int maxCount, CancellationToken cancellationToken = default) + public virtual async Task> GetWaitingEventsAsync(int maxCount, Expression>? filter = null, CancellationToken cancellationToken = default) { var dbContext = await DbContextProvider.GetDbContextAsync(); + Expression>? transformedFilter = null; + if (filter != null) + { + transformedFilter = InboxOutboxFilterExpressionTransformer.Transform(filter)!; + } + var outgoingEventRecords = await dbContext .IncomingEvents .AsNoTracking() .Where(x => !x.Processed) + .WhereIf(transformedFilter != null, transformedFilter!) .OrderBy(x => x.CreationTime) .Take(maxCount) .ToListAsync(cancellationToken: cancellationToken); diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventOutbox.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventOutbox.cs index fecfd1e8ce..a541698b3e 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventOutbox.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventOutbox.cs @@ -1,6 +1,7 @@ ๏ปฟusing System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -28,13 +29,20 @@ public class DbContextEventOutbox : IDbContextEventOutbox> GetWaitingEventsAsync(int maxCount, CancellationToken cancellationToken = default) + public virtual async Task> GetWaitingEventsAsync(int maxCount, Expression>? filter = null, CancellationToken cancellationToken = default) { var dbContext = (IHasEventOutbox)await DbContextProvider.GetDbContextAsync(); + Expression>? transformedFilter = null; + if (filter != null) + { + transformedFilter = InboxOutboxFilterExpressionTransformer.Transform(filter)!; + } + var outgoingEventRecords = await dbContext .OutgoingEvents .AsNoTracking() + .WhereIf(transformedFilter != null, transformedFilter!) .OrderBy(x => x.CreationTime) .Take(maxCount) .ToListAsync(cancellationToken: cancellationToken); diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/IncomingEventRecord.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/IncomingEventRecord.cs index 6cb37a72d5..be7da15890 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/IncomingEventRecord.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/IncomingEventRecord.cs @@ -8,6 +8,7 @@ namespace Volo.Abp.EntityFrameworkCore.DistributedEvents; public class IncomingEventRecord : BasicAggregateRoot, + IIncomingEventInfo, IHasExtraProperties, IHasCreationTime { diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/OutgoingEventRecord.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/OutgoingEventRecord.cs index 7272c9ac30..625a93b25a 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/OutgoingEventRecord.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/OutgoingEventRecord.cs @@ -8,6 +8,7 @@ namespace Volo.Abp.EntityFrameworkCore.DistributedEvents; public class OutgoingEventRecord : BasicAggregateRoot, + IOutgoingEventInfo, IHasExtraProperties, IHasCreationTime { diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/EfCoreRuntimeDatabaseMigratorBase.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/EfCoreRuntimeDatabaseMigratorBase.cs index 9b9719ff1b..ff557a2a73 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/EfCoreRuntimeDatabaseMigratorBase.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Migrations/EfCoreRuntimeDatabaseMigratorBase.cs @@ -135,7 +135,7 @@ public abstract class EfCoreRuntimeDatabaseMigratorBase : ITransient throw; } - Logger.LogWarning($"{ex.GetType().Name} has been thrown. The operation will be tried {maxTryCount} times more. Exception:\n{ex.Message}. Stack Trace:\n{ex.StackTrace}"); + Logger.LogWarning(ex, $"{ex.GetType().Name} has been thrown. The operation will be tried {maxTryCount} times more."); await Task.Delay(RandomHelper.GetRandom(MinValueToWaitOnFailure, MaxValueToWaitOnFailure)); diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventInbox.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventInbox.cs index 3700f74232..c154330495 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventInbox.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventInbox.cs @@ -1,5 +1,6 @@ ๏ปฟusing System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; @@ -9,7 +10,7 @@ public interface IEventInbox { Task EnqueueAsync(IncomingEventInfo incomingEvent); - Task> GetWaitingEventsAsync(int maxCount, CancellationToken cancellationToken = default); + Task> GetWaitingEventsAsync(int maxCount, Expression>? filter = null, CancellationToken cancellationToken = default); Task MarkAsProcessedAsync(Guid id); diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventOutbox.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventOutbox.cs index 018747945c..bc538ef839 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventOutbox.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventOutbox.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; @@ -9,7 +10,7 @@ public interface IEventOutbox { Task EnqueueAsync(OutgoingEventInfo outgoingEvent); - Task> GetWaitingEventsAsync(int maxCount, CancellationToken cancellationToken = default); + Task> GetWaitingEventsAsync(int maxCount, Expression>? filter = null, CancellationToken cancellationToken = default); Task DeleteAsync(Guid id); diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IIncomingEventInfo.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IIncomingEventInfo.cs new file mode 100644 index 0000000000..e52325b71f --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IIncomingEventInfo.cs @@ -0,0 +1,17 @@ +using System; +using Volo.Abp.Data; + +namespace Volo.Abp.EventBus.Distributed; + +public interface IIncomingEventInfo : IHasExtraProperties +{ + Guid Id { get; } + + string MessageId { get; } + + string EventName { get; } + + byte[] EventData { get; } + + DateTime CreationTime { get; } +} diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IOutgoingEventInfo.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IOutgoingEventInfo.cs new file mode 100644 index 0000000000..58dd4a9713 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IOutgoingEventInfo.cs @@ -0,0 +1,15 @@ +using System; +using Volo.Abp.Data; + +namespace Volo.Abp.EventBus.Distributed; + +public interface IOutgoingEventInfo : IHasExtraProperties +{ + Guid Id { get; } + + string EventName { get; } + + byte[] EventData { get; } + + DateTime CreationTime { get; } +} diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/InboxOutboxFilterExpressionTransformer.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/InboxOutboxFilterExpressionTransformer.cs new file mode 100644 index 0000000000..935e760546 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/InboxOutboxFilterExpressionTransformer.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq.Expressions; + +namespace Volo.Abp.EventBus.Distributed; + +public static class InboxOutboxFilterExpressionTransformer +{ + public static Expression> Transform(Expression> originalExpression) + { + var originalParam = originalExpression.Parameters[0]; + var newParam = Expression.Parameter(typeof(TTarget), originalParam.Name); + var body = ReplaceParameter(originalExpression.Body, originalParam, newParam); + return Expression.Lambda>(body, newParam); + } + + private static Expression ReplaceParameter(Expression body, ParameterExpression oldParam, ParameterExpression newParam) + { + var visitor = new ParameterReplacer(oldParam, newParam); + return visitor.Visit(body); + } + + private class ParameterReplacer : ExpressionVisitor + { + private readonly ParameterExpression _oldParam; + private readonly ParameterExpression _newParam; + + public ParameterReplacer(ParameterExpression oldParam, ParameterExpression newParam) + { + _oldParam = oldParam; + _newParam = newParam; + } + + protected override Expression VisitParameter(ParameterExpression node) + { + return node == _oldParam ? _newParam : base.VisitParameter(node); + } + } +} diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventInfo.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventInfo.cs index ffd135845a..1be24a3d02 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventInfo.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventInfo.cs @@ -4,7 +4,7 @@ using Volo.Abp.Data; namespace Volo.Abp.EventBus.Distributed; -public class IncomingEventInfo : IHasExtraProperties +public class IncomingEventInfo : IIncomingEventInfo { public static int MaxEventNameLength { get; set; } = 256; diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/OutgoingEventInfo.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/OutgoingEventInfo.cs index 43e9c42bf8..74b5bca7d4 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/OutgoingEventInfo.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/OutgoingEventInfo.cs @@ -4,7 +4,7 @@ using Volo.Abp.Data; namespace Volo.Abp.EventBus.Distributed; -public class OutgoingEventInfo : IHasExtraProperties +public class OutgoingEventInfo : IOutgoingEventInfo { public static int MaxEventNameLength { get; set; } = 256; diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/AbpEventBusRabbitMqModule.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/AbpEventBusRabbitMqModule.cs index 5d3db22726..92399b43b0 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/AbpEventBusRabbitMqModule.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/AbpEventBusRabbitMqModule.cs @@ -20,7 +20,7 @@ public class AbpEventBusRabbitMqModule : AbpModule { context .ServiceProvider - .GetRequiredService() + .GetRequiredService() .Initialize(); } } diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/IRabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/IRabbitMqDistributedEventBus.cs new file mode 100644 index 0000000000..fd88a098a2 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/IRabbitMqDistributedEventBus.cs @@ -0,0 +1,8 @@ +๏ปฟusing Volo.Abp.EventBus.Distributed; + +namespace Volo.Abp.EventBus.RabbitMq; + +public interface IRabbitMqDistributedEventBus : IDistributedEventBus +{ + void Initialize(); +} diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index 1c8012f529..c84aa6d9e9 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -23,8 +23,8 @@ namespace Volo.Abp.EventBus.RabbitMq; /* TODO: How to handle unsubscribe to unbind on RabbitMq (may not be possible for) */ [Dependency(ReplaceServices = true)] -[ExposeServices(typeof(IDistributedEventBus), typeof(RabbitMqDistributedEventBus))] -public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDependency +[ExposeServices(typeof(IDistributedEventBus), typeof(RabbitMqDistributedEventBus), typeof(IRabbitMqDistributedEventBus))] +public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDistributedEventBus, ISingletonDependency { protected AbpRabbitMqEventBusOptions AbpRabbitMqEventBusOptions { get; } protected IConnectionPool ConnectionPool { get; } @@ -72,7 +72,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDe EventTypes = new ConcurrentDictionary(); } - public void Initialize() + public virtual void Initialize() { Consumer = MessageConsumerFactory.Create( new ExchangeDeclareConfiguration( @@ -290,7 +290,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDe var eventName = EventNameAttribute.GetNameOrDefault(eventType); var body = Serializer.Serialize(eventData); - return PublishAsync( eventName, body, headersArguments, eventId, correlationId); + return PublishAsync(eventName, body, headersArguments, eventId, correlationId); } protected virtual Task PublishAsync( diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs index 67facf5d42..cc91cad5df 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs @@ -1,4 +1,5 @@ ๏ปฟusing System; +using System.Linq.Expressions; namespace Volo.Abp.EventBus.Distributed; @@ -14,11 +15,21 @@ public class AbpEventBusBoxesOptions /// public int InboxWaitingEventMaxCount { get; set; } + /// + /// Default: null, means all events + /// + public Expression>? InboxProcessorFilter { get; set; } + /// /// Default: 1000 /// public int OutboxWaitingEventMaxCount { get; set; } + /// + /// Default: null, means all events + /// + public Expression>? OutboxProcessorFilter { get; set; } + /// /// Period time of and /// Default: 2 seconds @@ -34,7 +45,7 @@ public class AbpEventBusBoxesOptions /// Default: 2 hours /// public TimeSpan WaitTimeToDeleteProcessedInboxEvents { get; set; } - + /// /// Default: true /// @@ -44,7 +55,9 @@ public class AbpEventBusBoxesOptions { CleanOldEventTimeIntervalSpan = TimeSpan.FromHours(6); InboxWaitingEventMaxCount = 1000; + InboxProcessorFilter = null; OutboxWaitingEventMaxCount = 1000; + OutboxProcessorFilter = null; PeriodTimeSpan = TimeSpan.FromSeconds(2); DistributedLockWaitDuration = TimeSpan.FromSeconds(15); WaitTimeToDeleteProcessedInboxEvents = TimeSpan.FromHours(2); diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessor.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessor.cs index e2c0a3c0c6..06014701a2 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessor.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -27,7 +28,7 @@ public class InboxProcessor : IInboxProcessor, ITransientDependency protected DateTime? LastCleanTime { get; set; } - protected string DistributedLockName { get; private set; } = default!; + protected string DistributedLockName { get; set; } = default!; public ILogger Logger { get; set; } protected CancellationTokenSource StoppingTokenSource { get; } protected CancellationToken StoppingToken { get; } @@ -60,7 +61,7 @@ public class InboxProcessor : IInboxProcessor, ITransientDependency await RunAsync(); } - public Task StartAsync(InboxConfig inboxConfig, CancellationToken cancellationToken = default) + public virtual Task StartAsync(InboxConfig inboxConfig, CancellationToken cancellationToken = default) { InboxConfig = inboxConfig; Inbox = (IEventInbox)ServiceProvider.GetRequiredService(inboxConfig.ImplementationType); @@ -69,7 +70,7 @@ public class InboxProcessor : IInboxProcessor, ITransientDependency return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken = default) + public virtual Task StopAsync(CancellationToken cancellationToken = default) { StoppingTokenSource.Cancel(); Timer.Stop(cancellationToken); @@ -92,7 +93,7 @@ public class InboxProcessor : IInboxProcessor, ITransientDependency while (true) { - var waitingEvents = await Inbox.GetWaitingEventsAsync(EventBusBoxesOptions.InboxWaitingEventMaxCount, StoppingToken); + var waitingEvents = await GetWaitingEventsAsync(); if (waitingEvents.Count <= 0) { break; @@ -129,6 +130,11 @@ public class InboxProcessor : IInboxProcessor, ITransientDependency } } + protected virtual async Task> GetWaitingEventsAsync() + { + return await Inbox.GetWaitingEventsAsync(EventBusBoxesOptions.InboxWaitingEventMaxCount, EventBusBoxesOptions.InboxProcessorFilter, StoppingToken); + } + protected virtual async Task DeleteOldEventsAsync() { if (LastCleanTime != null && LastCleanTime + EventBusBoxesOptions.CleanOldEventTimeIntervalSpan > Clock.Now) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/OutboxSender.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/OutboxSender.cs index 99ee06ca10..2e610d4471 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/OutboxSender.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/OutboxSender.cs @@ -22,7 +22,7 @@ public class OutboxSender : IOutboxSender, ITransientDependency protected IEventOutbox Outbox { get; private set; } = default!; protected OutboxConfig OutboxConfig { get; private set; } = default!; protected AbpEventBusBoxesOptions EventBusBoxesOptions { get; } - protected string DistributedLockName { get; private set; } = default!; + protected string DistributedLockName { get; set; } = default!; public ILogger Logger { get; set; } protected CancellationTokenSource StoppingTokenSource { get; } @@ -77,14 +77,14 @@ public class OutboxSender : IOutboxSender, ITransientDependency { while (true) { - var waitingEvents = await Outbox.GetWaitingEventsAsync(EventBusBoxesOptions.OutboxWaitingEventMaxCount, StoppingToken); + var waitingEvents = await GetWaitingEventsAsync(); if (waitingEvents.Count <= 0) { break; } Logger.LogInformation($"Found {waitingEvents.Count} events in the outbox."); - + if (EventBusBoxesOptions.BatchPublishOutboxEvents) { await PublishOutgoingMessagesInBatchAsync(waitingEvents); @@ -107,6 +107,11 @@ public class OutboxSender : IOutboxSender, ITransientDependency } } + protected virtual async Task> GetWaitingEventsAsync() + { + return await Outbox.GetWaitingEventsAsync(EventBusBoxesOptions.OutboxWaitingEventMaxCount, EventBusBoxesOptions.OutboxProcessorFilter, StoppingToken); + } + protected virtual async Task PublishOutgoingMessagesAsync(List waitingEvents) { foreach (var waitingEvent in waitingEvents) @@ -119,7 +124,7 @@ public class OutboxSender : IOutboxSender, ITransientDependency ); await Outbox.DeleteAsync(waitingEvent.Id); - + Logger.LogInformation($"Sent the event to the message broker with id = {waitingEvent.Id:N}"); } } @@ -129,9 +134,9 @@ public class OutboxSender : IOutboxSender, ITransientDependency await DistributedEventBus .AsSupportsEventBoxes() .PublishManyFromOutboxAsync(waitingEvents, OutboxConfig); - + await Outbox.DeleteManyAsync(waitingEvents.Select(x => x.Id).ToArray()); - + Logger.LogInformation($"Sent {waitingEvents.Count} events to message broker"); } } diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerExtensions.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerExtensions.cs index b954578ba6..4bc2a5433c 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerExtensions.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerExtensions.cs @@ -52,6 +52,11 @@ public static class FeatureCheckerExtensions return false; } + /// + /// Checks if the specified feature is enabled and throws an if it is not. + /// + /// The + /// The name of the feature to be checked. public static async Task CheckEnabledAsync(this IFeatureChecker featureChecker, string featureName) { if (!(await featureChecker.IsEnabledAsync(featureName))) @@ -61,6 +66,13 @@ public static class FeatureCheckerExtensions } } + /// + /// Checks if the specified features are enabled and throws an if they are not. + /// The check can either require all features to be enabled or just one, based on the parameter. + /// + /// The + /// True: Requires all features to be enabled. False: Requires at least one of the features to be enabled. + /// The names of the features to be checked. public static async Task CheckEnabledAsync(this IFeatureChecker featureChecker, bool requiresAll, params string[] featureNames) { if (featureNames.IsNullOrEmpty()) diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationSettingHelper.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationSettingHelper.cs index e24dd32199..860a0c9d96 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationSettingHelper.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationSettingHelper.cs @@ -8,17 +8,25 @@ public static class LocalizationSettingHelper /// Gets a setting value like "en-US;en" and returns as splitted values like ("en-US", "en"). /// /// + /// /// - public static (string cultureName, string uiCultureName) ParseLanguageSetting([NotNull] string settingValue) + public static (string cultureName, string uiCultureName) ParseLanguageSetting([NotNull] string settingValue, string defaultCultureName = "en") { Check.NotNull(settingValue, nameof(settingValue)); if (!settingValue.Contains(";")) { - return (settingValue, settingValue); + return CultureHelper.IsValidCultureCode(settingValue) + ? (settingValue, settingValue) + : (defaultCultureName, defaultCultureName); } var splitted = settingValue.Split(';'); - return (splitted[0], splitted[1]); + if (splitted.Length == 2 && CultureHelper.IsValidCultureCode(splitted[0]) && CultureHelper.IsValidCultureCode(splitted[1])) + { + return (splitted[0], splitted[1]); + } + + return (defaultCultureName, defaultCultureName); } } diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventInbox.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventInbox.cs index 55b40fa29b..6a73af27f5 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventInbox.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventInbox.cs @@ -1,6 +1,7 @@ ๏ปฟusing System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Options; @@ -50,16 +51,24 @@ public class MongoDbContextEventInbox : IMongoDbContextEventInb } [UnitOfWork] - public virtual async Task> GetWaitingEventsAsync(int maxCount, CancellationToken cancellationToken = default) + public virtual async Task> GetWaitingEventsAsync(int maxCount, Expression>? filter = null, CancellationToken cancellationToken = default) { var dbContext = await DbContextProvider.GetDbContextAsync(cancellationToken); + Expression>? transformedFilter = null; + if (filter != null) + { + transformedFilter = InboxOutboxFilterExpressionTransformer.Transform(filter)!; + } + var outgoingEventRecords = await dbContext .IncomingEvents .AsQueryable() .Where(x => !x.Processed) + .WhereIf(transformedFilter != null, transformedFilter!) .OrderBy(x => x.CreationTime) .Take(maxCount) + .As>() .ToListAsync(cancellationToken: cancellationToken); return outgoingEventRecords diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventOutbox.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventOutbox.cs index cc6c4d42df..57b59038b5 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventOutbox.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventOutbox.cs @@ -1,6 +1,7 @@ ๏ปฟusing System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; @@ -40,14 +41,22 @@ public class MongoDbContextEventOutbox : IMongoDbContextEventOu } [UnitOfWork] - public virtual async Task> GetWaitingEventsAsync(int maxCount, CancellationToken cancellationToken = default) + public virtual async Task> GetWaitingEventsAsync(int maxCount, Expression>? filter = null, CancellationToken cancellationToken = default) { var dbContext = (IHasEventOutbox)await MongoDbContextProvider.GetDbContextAsync(cancellationToken); + Expression>? transformedFilter = null; + if (filter != null) + { + transformedFilter = InboxOutboxFilterExpressionTransformer.Transform(filter)!; + } + var outgoingEventRecords = await dbContext .OutgoingEvents.AsQueryable() + .WhereIf(transformedFilter != null, transformedFilter!) .OrderBy(x => x.CreationTime) .Take(maxCount) + .As>() .ToListAsync(cancellationToken: cancellationToken); return outgoingEventRecords diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs index e3577bee6e..30733b1e24 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs @@ -55,10 +55,9 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency return specificMapper.Map(source); } - var result = TryToMapCollection(scope, source, default); - if (result != null) + if (TryToMapCollection(scope, source, default, out var collectionResult)) { - return result; + return collectionResult; } } @@ -100,10 +99,9 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency return specificMapper.Map(source, destination); } - var result = TryToMapCollection(scope, source, destination); - if (result != null) + if (TryToMapCollection(scope, source, destination, out var collectionResult)) { - return result; + return collectionResult; } } @@ -122,11 +120,12 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency return AutoMap(source, destination); } - protected virtual TDestination? TryToMapCollection(IServiceScope serviceScope, TSource source, TDestination? destination) + protected virtual bool TryToMapCollection(IServiceScope serviceScope, TSource source, TDestination? destination, out TDestination collectionResult) { if (!IsCollectionGenericType(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType)) { - return default; + collectionResult = default!; + return false; } var mapperType = typeof(IObjectMapper<,>).MakeGenericType(sourceArgumentType, destinationArgumentType); @@ -134,7 +133,8 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency if (specificMapper == null) { //skip, no specific mapper - return default; + collectionResult = default!; + return false; } var cacheKey = $"{mapperType.FullName}_{(destination == null ? "MapMethodWithSingleParameter" : "MapMethodWithDoubleParameters")}"; @@ -209,11 +209,13 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency if (destination != null && destination.GetType().IsArray) { //Return the new collection if destination is an array, We won't change array just same behavior as AutoMapper. - return (TDestination)result; + collectionResult = (TDestination)result; + return true; } //Return the destination if destination exists. The parameter reference equals with return object. - return destination ?? (TDestination)result; + collectionResult = destination ?? (TDestination)result; + return true; } protected virtual bool IsCollectionGenericType(out Type sourceArgumentType, out Type destinationArgumentType, out Type definitionGenericType) diff --git a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/AbpSettingOptions.cs b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/AbpSettingOptions.cs index c8a06736b6..e4bbb4fe00 100644 --- a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/AbpSettingOptions.cs +++ b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/AbpSettingOptions.cs @@ -11,10 +11,17 @@ public class AbpSettingOptions public HashSet DeletedSettings { get; } + /// + /// Default: true. + /// This is useful when you change of an existing setting definition to true and don't want to lose the original value. + /// + public bool ReturnOriginalValueIfDecryptFailed { get; set; } + public AbpSettingOptions() { DefinitionProviders = new TypeList(); ValueProviders = new TypeList(); DeletedSettings = new HashSet(); + ReturnOriginalValueIfDecryptFailed = true; } } diff --git a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingEncryptionService.cs b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingEncryptionService.cs index 45a3362d30..2af6b7b4af 100644 --- a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingEncryptionService.cs +++ b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingEncryptionService.cs @@ -1,6 +1,7 @@ ๏ปฟusing System; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Abp.Security.Encryption; @@ -10,10 +11,12 @@ public class SettingEncryptionService : ISettingEncryptionService, ITransientDep { protected IStringEncryptionService StringEncryptionService { get; } public ILogger Logger { get; set; } + protected IOptions Options { get; } - public SettingEncryptionService(IStringEncryptionService stringEncryptionService) + public SettingEncryptionService(IStringEncryptionService stringEncryptionService, IOptions options) { StringEncryptionService = stringEncryptionService; + Options = options; Logger = NullLogger.Instance; } @@ -40,7 +43,14 @@ public class SettingEncryptionService : ISettingEncryptionService, ITransientDep } catch (Exception e) { + if (Options.Value.ReturnOriginalValueIfDecryptFailed) + { + Logger.LogWarning(e, "Failed to decrypt the setting: {0}. Returning the original value...", settingDefinition.Name); + return encryptedValue; + } + Logger.LogException(e); + return string.Empty; } } diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/FodyWeavers.xml b/framework/src/Volo.Abp.Sms.TencentCloud/FodyWeavers.xml new file mode 100644 index 0000000000..2ad59ce186 --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/FodyWeavers.xsd b/framework/src/Volo.Abp.Sms.TencentCloud/FodyWeavers.xsd new file mode 100644 index 0000000000..ffa6fc4b78 --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.abppkg b/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.abppkg new file mode 100644 index 0000000000..f4bad072d2 --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.abppkg.analyze.json b/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.abppkg.analyze.json new file mode 100644 index 0000000000..83fe6d66b9 --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.abppkg.analyze.json @@ -0,0 +1,63 @@ +{ + "name": "Volo.Abp.Sms.TencentCloud", + "hash": "", + "contents": [ + { + "namespace": "Volo.Abp.Sms.TencentCloud", + "dependsOnModules": [ + { + "declaringAssemblyName": "Volo.Abp.Sms", + "namespace": "Volo.Abp.Sms", + "name": "AbpSmsModule" + } + ], + "implementingInterfaces": [ + { + "name": "IAbpModule", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IAbpModule" + }, + { + "name": "IOnPreApplicationInitialization", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IOnPreApplicationInitialization" + }, + { + "name": "IOnApplicationInitialization", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IOnApplicationInitialization" + }, + { + "name": "IOnPostApplicationInitialization", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IOnPostApplicationInitialization" + }, + { + "name": "IOnApplicationShutdown", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IOnApplicationShutdown" + }, + { + "name": "IPreConfigureServices", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IPreConfigureServices" + }, + { + "name": "IPostConfigureServices", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IPostConfigureServices" + } + ], + "contentType": "abpModule", + "name": "AbpSmsTencentCloudModule", + "summary": null + } + ] +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.csproj b/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.csproj new file mode 100644 index 0000000000..3077ff392e --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/Volo.Abp.Sms.TencentCloud.csproj @@ -0,0 +1,26 @@ + + + + + + netstandard2.0;netstandard2.1;net8.0;net9.0 + enable + Nullable + Volo.Abp.Sms.TencentCloud + Volo.Abp.Sms.TencentCloud + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudModule.cs b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudModule.cs new file mode 100644 index 0000000000..b846a53e9d --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudModule.cs @@ -0,0 +1,15 @@ +๏ปฟusing Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; + +namespace Volo.Abp.Sms.TencentCloud; + +[DependsOn(typeof(AbpSmsModule))] +public class AbpSmsTencentCloudModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + Configure(configuration.GetSection("AbpTencentCloudSms")); + } +} diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/AbpTencentCloudSmsOptions.cs b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/AbpTencentCloudSmsOptions.cs new file mode 100644 index 0000000000..cec86d9b36 --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/AbpTencentCloudSmsOptions.cs @@ -0,0 +1,14 @@ +namespace Volo.Abp.Sms.TencentCloud; + +public class AbpTencentCloudSmsOptions +{ + public string SmsSdkAppId { get; set; } = default!; + + public string SecretKey { get; set; } = default!; + + public string SecretId { get; set; } = default!; + + public string Endpoint { get; set; } = "sms.tencentcloudapi.com"; + + public string Region { get; set; } = "ap-guangzhou"; +} diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/TencentCloudSmsProperties.cs b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/TencentCloudSmsProperties.cs new file mode 100644 index 0000000000..7253a85ba6 --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/TencentCloudSmsProperties.cs @@ -0,0 +1,8 @@ +๏ปฟnamespace Volo.Abp.Sms.TencentCloud; + +public static class TencentCloudSmsProperties +{ + public const string SignName = "SignName"; + + public const string TemplateId = "TemplateId"; +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/TencentCloudSmsSender.cs b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/TencentCloudSmsSender.cs new file mode 100644 index 0000000000..fa3c585576 --- /dev/null +++ b/framework/src/Volo.Abp.Sms.TencentCloud/Volo/Abp/Sms/TencentCloud/TencentCloudSmsSender.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using TencentCloud.Common; +using TencentCloud.Common.Profile; +using TencentCloud.Sms.V20210111; +using TencentCloud.Sms.V20210111.Models; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Sms.TencentCloud; + +public class TencentCloudSmsSender : ISmsSender, ITransientDependency +{ + protected AbpTencentCloudSmsOptions Options { get; } + + public TencentCloudSmsSender(IOptionsMonitor options) + { + Options = options.CurrentValue; + } + + public virtual async Task SendAsync(SmsMessage smsMessage) + { + var client = CreateClient(); + + await client.SendSms(new SendSmsRequest() + { + SmsSdkAppId = Options.SmsSdkAppId, + SignName = smsMessage.Properties.GetOrDefault(TencentCloudSmsProperties.SignName) as string, + TemplateId = smsMessage.Properties.GetOrDefault(TencentCloudSmsProperties.TemplateId) as string, + TemplateParamSet = smsMessage.Text.Split(','), + PhoneNumberSet = [smsMessage.PhoneNumber] + }); + } + + protected virtual SmsClient CreateClient() + { + var credential = new Credential + { + SecretId = Options.SecretId, + SecretKey = Options.SecretKey + }; + var clientProfile = new ClientProfile + { + HttpProfile = new HttpProfile + { + Endpoint = Options.Endpoint + } + }; + return new SmsClient(credential, Options.Region, clientProfile); + } +} diff --git a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Basic_Tests.cs b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Basic_Tests.cs index cdaf58b9a4..82a0369aa0 100644 --- a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Basic_Tests.cs +++ b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Basic_Tests.cs @@ -1,4 +1,5 @@ -๏ปฟusing Microsoft.Extensions.DependencyInjection; +๏ปฟusing System; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using Volo.Abp.AutoMapper.SampleClasses; using Volo.Abp.ObjectMapping; @@ -36,6 +37,13 @@ public class AbpAutoMapperModule_Basic_Tests : AbpIntegratedTest(MyEnum.Value3); + dto.ShouldBe(MyEnumDto.Value2); //Value2 is same as Value3 + } + //[Fact] TODO: Disabled because of https://github.com/AutoMapper/AutoMapper/pull/2379#issuecomment-355899664 /*public void Should_Not_Map_Objects_With_AutoMap_Attributes() { diff --git a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnum.cs b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnum.cs new file mode 100644 index 0000000000..d8b655b4d7 --- /dev/null +++ b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnum.cs @@ -0,0 +1,8 @@ +๏ปฟnamespace Volo.Abp.AutoMapper.SampleClasses; + +public enum MyEnum +{ + Value1 = 1, + Value2, + Value3 +} diff --git a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnumDto.cs b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnumDto.cs new file mode 100644 index 0000000000..fb33818660 --- /dev/null +++ b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnumDto.cs @@ -0,0 +1,8 @@ +๏ปฟnamespace Volo.Abp.AutoMapper.SampleClasses; + +public enum MyEnumDto +{ + Value1 = 2, + Value2, + Value3 +} diff --git a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs index f36322ddaa..295ff6b08e 100644 --- a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs +++ b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs @@ -9,6 +9,8 @@ public class MyMapProfile : Profile { CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap() .MapExtraProperties(ignoredProperties: new[] { "CityName" }); diff --git a/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo.Abp.Sms.TencentCloud.Tests.abppkg b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo.Abp.Sms.TencentCloud.Tests.abppkg new file mode 100644 index 0000000000..a686451fbc --- /dev/null +++ b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo.Abp.Sms.TencentCloud.Tests.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.test" +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo.Abp.Sms.TencentCloud.Tests.csproj b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo.Abp.Sms.TencentCloud.Tests.csproj new file mode 100644 index 0000000000..cd63f915d1 --- /dev/null +++ b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo.Abp.Sms.TencentCloud.Tests.csproj @@ -0,0 +1,26 @@ + + + + + + net9.0 + + + + + + + + + + + + + + + + Always + + + + diff --git a/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudTestBase.cs b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudTestBase.cs new file mode 100644 index 0000000000..dfc666f21f --- /dev/null +++ b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudTestBase.cs @@ -0,0 +1,11 @@ +using Volo.Abp.Testing; + +namespace Volo.Abp.Sms.TencentCloud; + +public class AbpSmsTencentCloudTestBase : AbpIntegratedTest +{ + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } +} diff --git a/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudTestsModule.cs b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudTestsModule.cs new file mode 100644 index 0000000000..5062c18867 --- /dev/null +++ b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/AbpSmsTencentCloudTestsModule.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; + +namespace Volo.Abp.Sms.TencentCloud; + +[DependsOn(typeof(AbpSmsTencentCloudModule))] +public class AbpSmsTencentCloudTestsModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + Configure(configuration.GetSection("AbpTencentCloudSms")); + } +} diff --git a/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/TencentCloudSmsSenderTests.cs b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/TencentCloudSmsSenderTests.cs new file mode 100644 index 0000000000..9198f9b86d --- /dev/null +++ b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/Volo/Abp/Sms/TencentCloud/TencentCloudSmsSenderTests.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Volo.Abp.Sms.TencentCloud; + +public class TencentCloudSmsSenderTests : AbpSmsTencentCloudTestBase +{ + private readonly ISmsSender _smsSender; + private readonly IConfiguration _configuration; + + public TencentCloudSmsSenderTests() + { + _configuration = GetRequiredService(); + _smsSender = GetRequiredService(); + } + + [Fact] + public async Task SendSms_Test() + { + var config = _configuration.GetSection("AbpTencentCloudSms"); + + // Please fill in the real parameters in the appsettings.json file. + if (config["SecretId"] == "") + { + return; + } + + var msg = new SmsMessage(config["TargetPhoneNumber"], + config["TemplateParam"]); + msg.Properties.Add(TencentCloudSmsProperties.SignName, config["SignName"]); + msg.Properties.Add(TencentCloudSmsProperties.TemplateId, config["TemplateId"]); + + await _smsSender.SendAsync(msg); + } +} diff --git a/framework/test/Volo.Abp.Sms.TencenCloud.Tests/appsettings.json b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/appsettings.json new file mode 100644 index 0000000000..44d4611caf --- /dev/null +++ b/framework/test/Volo.Abp.Sms.TencenCloud.Tests/appsettings.json @@ -0,0 +1,13 @@ +{ + "AbpTencentCloudSms": { + "SecretId": "", + "SecretKey": "", + "Region": "", + "SmsSdkAppId": "", + "Endpoint": "", + "TargetPhoneNumber": "", + "SignName": "", + "TemplateId": "", + "TemplateParam": "" + } +} \ No newline at end of file diff --git a/latest-versions.json b/latest-versions.json index e2c60800a3..43d4fbb9ac 100644 --- a/latest-versions.json +++ b/latest-versions.json @@ -1,13 +1,22 @@ [ + { + "version": "9.0.2", + "releaseDate": "", + "type": "stable", + "message": "", + "leptonx": { + "version": "4.0.3" + } + }, { "version": "9.0.1", "releaseDate": "", "type": "stable", "message": "", "leptonx": { - "version": "4.0.1" + "version": "4.0.2" } - }, + }, { "version": "9.0.0", "releaseDate": "", @@ -26,4 +35,4 @@ "version": "3.3.1" } } -] \ No newline at end of file +] diff --git a/modules/basic-theme/Volo.Abp.BasicTheme.sln b/modules/basic-theme/Volo.Abp.BasicTheme.sln index 2e65f7a98d..ac0ab4ed3b 100644 --- a/modules/basic-theme/Volo.Abp.BasicTheme.sln +++ b/modules/basic-theme/Volo.Abp.BasicTheme.sln @@ -20,6 +20,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Mvc.UI. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BasicTheme.Installer", "src\Volo.Abp.BasicTheme.Installer\Volo.Abp.BasicTheme.Installer.csproj", "{3068A87F-3348-4981-8241-2630BC496117}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling", "src\Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling\Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling.csproj", "{D02053D9-10EF-4717-A792-A53F83347816}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,6 +60,10 @@ Global {3068A87F-3348-4981-8241-2630BC496117}.Debug|Any CPU.Build.0 = Debug|Any CPU {3068A87F-3348-4981-8241-2630BC496117}.Release|Any CPU.ActiveCfg = Release|Any CPU {3068A87F-3348-4981-8241-2630BC496117}.Release|Any CPU.Build.0 = Release|Any CPU + {D02053D9-10EF-4717-A792-A53F83347816}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D02053D9-10EF-4717-A792-A53F83347816}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D02053D9-10EF-4717-A792-A53F83347816}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D02053D9-10EF-4717-A792-A53F83347816}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {C8068E7F-4A04-4755-8976-C2A4C0ADC708} = {ED6D078F-B0A2-48E8-A09D-3B7CDF6CE3D1} @@ -68,5 +74,6 @@ Global {51B491ED-F959-4974-A876-528B5F16BC92} = {0BC55E3B-4964-48E3-A390-2ADD37980149} {8C336CB8-F7A9-4203-AE55-D8F5FDB2A958} = {0BC55E3B-4964-48E3-A390-2ADD37980149} {3068A87F-3348-4981-8241-2630BC496117} = {ED6D078F-B0A2-48E8-A09D-3B7CDF6CE3D1} + {D02053D9-10EF-4717-A792-A53F83347816} = {ED6D078F-B0A2-48E8-A09D-3B7CDF6CE3D1} EndGlobalSection EndGlobal diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/AbpAspNetCoreComponentsWebAssemblyBasicThemeBundlingModule.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/AbpAspNetCoreComponentsWebAssemblyBasicThemeBundlingModule.cs new file mode 100644 index 0000000000..b6fcb74e34 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/AbpAspNetCoreComponentsWebAssemblyBasicThemeBundlingModule.cs @@ -0,0 +1,20 @@ +๏ปฟusing Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling; + +[DependsOn( + typeof(AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule) +)] +public class AbpAspNetCoreComponentsWebAssemblyBasicThemeBundlingModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + var globalStyles = options.StyleBundles.Get(BlazorWebAssemblyStandardBundles.Styles.Global); + globalStyles.AddContributors(typeof(BasicThemeBundleStyleContributor)); + }); + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/BasicThemeBundleStyleContributor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/BasicThemeBundleStyleContributor.cs new file mode 100644 index 0000000000..de65538929 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/BasicThemeBundleStyleContributor.cs @@ -0,0 +1,12 @@ +๏ปฟusing System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling; + +public class BasicThemeBundleStyleContributor : BundleContributor +{ + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web.BasicTheme/libs/abp/css/theme.css"); + } +} diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/FodyWeavers.xml b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/FodyWeavers.xml new file mode 100644 index 0000000000..7e9f94ead6 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/FodyWeavers.xsd b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/FodyWeavers.xsd new file mode 100644 index 0000000000..ffa6fc4b78 --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling.csproj b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling.csproj new file mode 100644 index 0000000000..edf08d8ebe --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling.csproj @@ -0,0 +1,15 @@ +๏ปฟ + + + + + + net9.0 + Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling + + + + + + + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/AbpAspNetCoreComponentsWebAssemblyBasicThemeModule.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/AbpAspNetCoreComponentsWebAssemblyBasicThemeModule.cs index cbc24669d8..08454376c2 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/AbpAspNetCoreComponentsWebAssemblyBasicThemeModule.cs +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/AbpAspNetCoreComponentsWebAssemblyBasicThemeModule.cs @@ -3,6 +3,7 @@ using Volo.Abp.AspNetCore.Components.Web; using Volo.Abp.AspNetCore.Components.Web.BasicTheme; using Volo.Abp.AspNetCore.Components.Web.Theming.Routing; using Volo.Abp.AspNetCore.Components.Web.Theming.Toolbars; +using Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.Bundling; using Volo.Abp.AspNetCore.Components.WebAssembly.Theming; using Volo.Abp.Http.Client.IdentityModel.WebAssembly; using Volo.Abp.Modularity; @@ -10,6 +11,7 @@ using Volo.Abp.Modularity; namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme; [DependsOn( + typeof(AbpAspNetCoreComponentsWebAssemblyBasicThemeBundlingModule), typeof(AbpAspNetCoreComponentsWebBasicThemeModule), typeof(AbpAspNetCoreComponentsWebAssemblyThemingModule), typeof(AbpHttpClientIdentityModelWebAssemblyModule) diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeBundleContributor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeBundleContributor.cs index 67b29987f1..ce07a26c63 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeBundleContributor.cs +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/BasicThemeBundleContributor.cs @@ -1,7 +1,9 @@ -๏ปฟusing Volo.Abp.Bundling; +using System; +using Volo.Abp.Bundling; namespace Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme; +[Obsolete("This class is obsolete and will be removed in the future versions. Use GlobalAssets instead.")] public class BasicThemeBundleContributor : IBundleContributor { public void AddScripts(BundleContext context) diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.csproj b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.csproj index d7ee34d13c..c070361f08 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.csproj +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme.csproj @@ -12,6 +12,7 @@ + diff --git a/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/NavigationNode.cs b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/NavigationNode.cs index 2f1afce342..f19b9f41df 100644 --- a/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/NavigationNode.cs +++ b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Documents/NavigationNode.cs @@ -15,6 +15,12 @@ namespace Volo.Docs.Documents [JsonPropertyName("items")] public List Items { get; set; } + + [JsonPropertyName("isLazyExpandable")] + public bool IsLazyExpandable { get; set; } + + [JsonPropertyName("isIndex")] + public bool IsIndex { get; set; } public bool IsLeaf => !HasChildItems; diff --git a/modules/docs/src/Volo.Docs.Web/Areas/Documents/DocumentNavigationController.cs b/modules/docs/src/Volo.Docs.Web/Areas/Documents/DocumentNavigationController.cs new file mode 100644 index 0000000000..cac52e8d5f --- /dev/null +++ b/modules/docs/src/Volo.Docs.Web/Areas/Documents/DocumentNavigationController.cs @@ -0,0 +1,80 @@ +using System; +using System.Threading.Tasks; +using Asp.Versioning; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Docs.Areas.Models.DocumentNavigation; +using Volo.Docs.Documents; +using Volo.Docs.Utils; + +namespace Volo.Docs.Areas.Documents; + +[RemoteService(Name = DocsRemoteServiceConsts.RemoteServiceName)] +[Area(DocsRemoteServiceConsts.ModuleName)] +[ControllerName("DocumentNavigation")] +[Route("/docs/document-navigation")] +public class DocumentNavigationController : AbpController +{ + private readonly IDocumentAppService _documentAppService; + private readonly IDocsLinkGenerator _docsLinkGenerator; + + public DocumentNavigationController(IDocumentAppService documentAppService, IDocsLinkGenerator docsLinkGenerator) + { + _documentAppService = documentAppService; + _docsLinkGenerator = docsLinkGenerator; + } + + [HttpGet] + [Route("")] + public virtual async Task GetNavigationAsync(GetNavigationNodeWithLinkModel input) + { + var navigationNode = await _documentAppService.GetNavigationAsync(new GetNavigationDocumentInput + { + LanguageCode = input.LanguageCode, + Version = input.Version, + ProjectId = input.ProjectId + }); + + NormalPath(navigationNode, input); + + return navigationNode; + } + + protected virtual void NormalPath(NavigationNode node, GetNavigationNodeWithLinkModel input) + { + if (node.HasChildItems) + { + foreach (var item in node.Items) + { + NormalPath(item, input); + } + } + + if (UrlHelper.IsExternalLink(node.Path)) + { + return; + } + + node.Path = RemoveFileExtensionFromPath(node.Path, input.ProjectFormat); + if (node.Path.IsNullOrWhiteSpace()) + { + node.Path = "javascript:;"; + return; + } + + node.Path = _docsLinkGenerator.GenerateLink(input.ProjectName, input.LanguageCode, input.RouteVersion, node.Path); + } + + private string RemoveFileExtensionFromPath(string path, string projectFormat) + { + if (path == null) + { + return null; + } + + return path.EndsWith("." + projectFormat) + ? path.Left(path.Length - projectFormat.Length - 1) + : path; + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Web/Areas/Documents/TagHelpers/TreeTagHelper.cs b/modules/docs/src/Volo.Docs.Web/Areas/Documents/TagHelpers/TreeTagHelper.cs index 6d2cb8c9ba..e6c75d0161 100644 --- a/modules/docs/src/Volo.Docs.Web/Areas/Documents/TagHelpers/TreeTagHelper.cs +++ b/modules/docs/src/Volo.Docs.Web/Areas/Documents/TagHelpers/TreeTagHelper.cs @@ -72,11 +72,14 @@ namespace Volo.Docs.Areas.Documents.TagHelpers var isAnyNodeOpenedInThisLevel = IsAnyNodeOpenedInThisLevel(node); - node.Items?.ForEach(innerNode => + if (!node.IsLazyExpandable || isAnyNodeOpenedInThisLevel) { - content += GetParentNode(innerNode, isAnyNodeOpenedInThisLevel); - }); - + node.Items?.ForEach(innerNode => + { + content += GetParentNode(innerNode, isAnyNodeOpenedInThisLevel); + }); + } + var result = node.IsEmpty ? content : GetLeafNode(node, content); return result; @@ -121,6 +124,11 @@ namespace Volo.Docs.Areas.Documents.TagHelpers listItemCss += " selected-tree"; } + if (node.IsLazyExpandable) + { + listItemCss += " lazy-expand"; + } + string listInnerItem; if (node.Path.IsNullOrEmpty() && node.IsLeaf) { diff --git a/modules/docs/src/Volo.Docs.Web/Areas/Models/DocumentNavigation/GetNavigationNodeWithLinkModel.cs b/modules/docs/src/Volo.Docs.Web/Areas/Models/DocumentNavigation/GetNavigationNodeWithLinkModel.cs new file mode 100644 index 0000000000..085cc2de0e --- /dev/null +++ b/modules/docs/src/Volo.Docs.Web/Areas/Models/DocumentNavigation/GetNavigationNodeWithLinkModel.cs @@ -0,0 +1,28 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Validation; +using Volo.Docs.Language; +using Volo.Docs.Projects; + +namespace Volo.Docs.Areas.Models.DocumentNavigation; + +public class GetNavigationNodeWithLinkModel +{ + public Guid ProjectId { get; set; } + + [DynamicStringLength(typeof(ProjectConsts), nameof(ProjectConsts.MaxVersionNameLength))] + public string Version { get; set; } + + [Required] + [DynamicStringLength(typeof(LanguageConsts), nameof(LanguageConsts.MaxLanguageCodeLength))] + public string LanguageCode { get; set; } + + [Required] + public string ProjectName { get; set; } + + [Required] + public string ProjectFormat { get; set; } + + [Required] + public string RouteVersion { get; set; } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml index 2167119a0d..fb1f232cb3 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml @@ -16,6 +16,7 @@ @using Volo.Abp.AspNetCore.Mvc.UI.Theming @using Volo.Docs @using Volo.Docs.Areas.Documents.TagHelpers +@using Volo.Docs.Documents @using Volo.Docs.Localization @using Volo.Docs.Pages.Documents.Project @using Volo.Docs.Pages.Documents.Shared.ErrorComponent @@ -52,6 +53,17 @@ @if (Model.LoadSuccess) { + @@ -292,6 +304,73 @@ + @if (Model.Navigation != null && Model.Navigation.Items != null) + { + var currentNode = Model.Navigation.Items.FirstOrDefault(n => n.IsSelected(Model.DocumentNameWithExtension)); + var navigation = Model.Navigation.FindNavigation(Model.DocumentNameWithExtension); + var documentExtension = System.IO.Path.GetExtension(Model.DocumentNameWithExtension); + NavigationNode previousNode = null; + if (currentNode != null) + { + while (currentNode != null) + { + if (!string.IsNullOrWhiteSpace(currentNode.Path) && previousNode != currentNode) + { + previousNode = currentNode; + PrintNode(currentNode); + } + else + { + var indexDocument = FindIndexNode(currentNode); + if (indexDocument != null && indexDocument != previousNode) + { + PrintNode(indexDocument, currentNode.Text); + } + else if(currentNode != previousNode) + { + + } + + previousNode = indexDocument; + } + + currentNode = currentNode?.Items?.FirstOrDefault(n => n.IsSelected(Model.DocumentNameWithExtension)); + } + } + + void PrintNode(NavigationNode node, string text = null) + { + var extension = System.IO.Path.GetExtension(node.Path); + var path = string.IsNullOrWhiteSpace(extension) ? node.Path : node.Path.Substring(0, node.Path.Length - extension.Length); + + } + + NavigationNode FindIndexNode(NavigationNode node) + { + var nextNode = node.Items?.FirstOrDefault(n => n.IsSelected(Model.DocumentNameWithExtension)); + while (nextNode != null && string.IsNullOrEmpty(nextNode.Path)) + { + nextNode = nextNode.Items.FirstOrDefault(n => n.IsSelected(Model.DocumentNameWithExtension)); + } + + if(nextNode == null) + { + return node.Items?.FirstOrDefault(n => n.IsIndex); + } + + var lastSlashIndex = nextNode.Path.LastIndexOf('/'); + var path = lastSlashIndex < 0 ? nextNode.Path : nextNode.Path.Substring(0, lastSlashIndex); + return node.Items.FirstOrDefault(n => n.Path != null && (n.IsSelected(path + documentExtension) || n.IsSelected(path + "/index" + documentExtension))) ?? node.Items.FirstOrDefault(n => n.IsIndex); + } + }