@ -0,0 +1,195 @@ |
|||
# ABP Platform 9.2 RC Has Been Released |
|||
|
|||
We are happy to release [ABP](https://abp.io) version **9.2 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version. |
|||
|
|||
Try this version and provide feedback for a more stable version of ABP v9.2! Thanks to you in advance. |
|||
|
|||
## Get Started with the 9.2 RC |
|||
|
|||
You can check the [Get Started page](https://abp.io/get-started) to see how to get started with ABP. You can either download [ABP Studio](https://abp.io/get-started#abp-studio-tab) (**recommended**, if you prefer a user-friendly GUI application - desktop application) or use the [ABP CLI](https://abp.io/docs/latest/cli). |
|||
|
|||
By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI: |
|||
|
|||
 |
|||
|
|||
## Migration Guide |
|||
|
|||
There are a few breaking changes in this version that may affect your application. Please read the migration guide carefully, if you are upgrading from v9.x or earlier: [ABP Version 9.2 Migration Guide](https://abp.io/docs/9.2/release-info/migration-guides/abp-9-2) |
|||
|
|||
## What's New with ABP v9.2? |
|||
|
|||
In this section, I will introduce some major features released in this version. |
|||
Here is a brief list of titles explained in the next sections: |
|||
|
|||
* Added `ApplicationName` Property to Isolate Background Jobs & Background Workers |
|||
* Docs Module: Added "Alternative Words" to Filter Items |
|||
* Introducing the Bunny BLOB Storage Provider |
|||
* Upgraded `MongoDB.Driver` to v3.1.0 |
|||
* Using Timezone Settings to Display Datetime |
|||
* Identity Pro Module: Require Email Verification to Register |
|||
* Switching users during OAuth login |
|||
|
|||
### Added ApplicationName Property to Isolate Background Jobs & Background Workers |
|||
|
|||
ABP's [Background Jobs Module](https://abp.io/docs/latest/modules/background-jobs) has been enhanced with a new `ApplicationName` property that helps isolate jobs and workers across multiple applications sharing the same database. |
|||
|
|||
Previously, when different applications used the BackgroundJobs module and shared a database, an application might encounter jobs that didn't belong to it. This would lead to failed processing attempts and marking jobs as `IsAbandoned = true` with a "Undefined background job for the job name" error, preventing these jobs from ever being executed. |
|||
|
|||
With the new `ApplicationName` property, applications now properly filter jobs at the repository level, ensuring each application only processes job types it recognizes. This prevents the incorrect abandonment of jobs and ensures consistent behavior in multi-application scenarios. |
|||
|
|||
You can set `ApplicationName` of `AbpBackgroundJobWorkerOptions` to your application name to isolate jobs and workers across multiple applications sharing the same database: |
|||
|
|||
```csharp |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
PreConfigure<AbpBackgroundJobWorkerOptions>(options => |
|||
{ |
|||
options.ApplicationName = context.Services.GetApplicationName()!; |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
> For more information, please refer to the [Background Jobs Module](https://abp.io/docs/latest/modules/background-jobs) documentation and the [PR](https://github.com/abpframework/abp/pull/22169) that added this feature. |
|||
|
|||
### Docs Module: Added "Alternative Words" to Filter Items |
|||
|
|||
[ABP's Docs Module](https://abp.io/docs/9.2/modules/docs) now supports "alternative words" to enhance the search functionality when filtering documentation items. This feature addresses a common user experience issue where users might search using terminology different from what appears in the documentation. |
|||
|
|||
For example, when a user searches for "Error" in the documentation, they may actually be looking for content related to "Exception Handling." With this new feature, documentation items can now be configured with alternative keywords that are considered during filtering. |
|||
|
|||
The implementation allows defining optional "keywords" for items in the navigation tree. For example: |
|||
|
|||
```json |
|||
{ |
|||
"text": "Exception Handling", |
|||
"path": "framework/fundamentals/exception-handling.md", |
|||
"keywords": ["Error", "Another Value"] |
|||
} |
|||
``` |
|||
|
|||
When users search or filter content, the system now considers both the original text and these alternative keywords, improving discoverability of relevant documentation sections. This enhancement makes the documentation more accessible and user-friendly, especially for newcomers who might not be familiar with the exact terminology used in the ABP documentation. |
|||
|
|||
### Introducing the Bunny BLOB Storage Provider |
|||
|
|||
ABP v9.2 RC introduces a new BLOB storage provider for [Bunny Storage](https://bunny.net/storage/), a global edge storage solution. This addition expands ABP's BLOB Storage options beyond the existing providers like Azure, AWS, and others. |
|||
|
|||
The [Bunny BLOB Storage Provider](https://abp.io/docs/9.2/framework/infrastructure/blob-storing/bunny) allows ABP applications to seamlessly integrate with Bunny's CDN-backed storage service, which offers high-performance content delivery through its global network. |
|||
|
|||
To use this new provider, you'll need to: |
|||
|
|||
* Run `abp add-package Volo.Abp.BlobStoring.Bunny` command. |
|||
* And then configure the provider in your module's `ConfigureServices` method: |
|||
|
|||
```csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseBunny(bunny => |
|||
{ |
|||
bunny.StorageZoneName = "your-storage-zone"; |
|||
bunny.ApiKey = "your-api-key"; |
|||
bunny.Region = "your-region"; // de, ny, la, sg, or sy |
|||
}); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
This integration provides ABP applications with an efficient and globally distributed storage solution, particularly beneficial for applications requiring fast content delivery across different geographical regions. To use this new provider and make the related configurations, you can refer to the [Bunny Storage Provider](https://abp.io/docs/9.2/framework/infrastructure/blob-storing/bunny) documentation always. |
|||
|
|||
> This new BLOB Storage provider is contributed by [@suhaib-mousa](https://github.com/suhaib-mousa). Thanks to him for his contribution! |
|||
> We are always happy to see the community contributing to the ABP Framework and encouraging them to contribute more. |
|||
|
|||
### Upgraded `MongoDB.Driver` to `v3.1.0` |
|||
|
|||
ABP v9.2 RC includes an upgrade to `MongoDB.Driver` version `3.1.0`. This significant version bump from previous releases brings several improvements and new features that benefit ABP applications using MongoDB as their database. |
|||
|
|||
The upgrade provides: |
|||
|
|||
* Async/Await Support: Write non-blocking, asynchronous code easily. |
|||
* Fluent API: Build queries and updates intuitively with Builders. |
|||
* LINQ Support: Use LINQ for querying MongoDB collections. |
|||
* and more ... |
|||
|
|||
> For more information, please refer to the [MongoDB.Driver release notes](https://github.com/mongodb/mongo-csharp-driver/releases/tag/v3.1.0). |
|||
|
|||
We have prepared a [migration guide](https://abp.io/docs/9.2/release-info/migration-guides/MongoDB-Driver-2-to-3) for this upgrade. Please refer to it to learn more about the changes and how to migrate your application. |
|||
|
|||
### Using Timezone Settings to Display Datetime |
|||
|
|||
A significant enhancement in ABP v9.2 is the ability to use timezone settings to display `DateTime` values according to the user's or application's configured timezone. Introduced in `v9.2.0-rc.2`, this feature addresses the common challenge of ensuring users see accurate time information, regardless of their geographical location. |
|||
|
|||
Previously, `DateTime` values were often shown in the server's timezone or in UTC, which could cause confusion for users in different timezones. With this new feature, ABP applications can now respect the configured timezone and automatically convert and display datetime values accordingly. |
|||
|
|||
**Before setting the timezone:** |
|||
|
|||
Consider a scenario where you have a list of books, and the `CreationTime` property is displayed without any timezone consideration. It may appear in the server's default timezone: |
|||
|
|||
 |
|||
*(Screenshot of a Books page showing the CreationTime in a default timezone.)* |
|||
|
|||
**Setting the timezone:** |
|||
|
|||
To set the timezone, start by configuring the `AbpClockOptions` in your module's `ConfigureServices` method: |
|||
|
|||
```csharp |
|||
Configure<AbpClockOptions>(options => |
|||
{ |
|||
options.Kind = DateTimeKind.Utc; |
|||
}); |
|||
``` |
|||
|
|||
> By setting the `Kind` property of `AbpClockOptions` to `DateTimeKind.Utc`, ABP will normalize all datetime values. Times stored in the database and returned to the frontend will be in UTC. Additionally, the `SupportsMultipleTimezone` property of the `IClock` service will be **true**, and you’ll be able to configure the timezone from the UI under the _Settings_ page. |
|||
|
|||
After setting the `Kind` property, you can run your application and configure the timezone from the UI under the _Settings_ page. For example, set the timezone to "Asia/Tokyo (+09:00)": |
|||
|
|||
 |
|||
*(Screenshot showing the configuration setting for Abp.Timing.TimeZone, set to "Asia/Tokyo (+09:00)".)* |
|||
|
|||
> ABP provides the `Abp.Timing.TimeZone` setting, which allows you to configure the desired timezone at the application, tenant, or user level and this setting can be configured in the UI under the _Settings_ page, inside of the _Time Zone_ tab. |
|||
|
|||
**After setting the timezone:** |
|||
|
|||
Once the timezone is configured, ABP automatically handles the conversion and display of `DateTime` values. The `CreationTime` on the _Books_ page will now be shown in the **"Asia/Tokyo"** timezone. |
|||
|
|||
 |
|||
*(Screenshot of the _Books_ page after setting the timezone, showing the `CreationTime` adjusted to the "Asia/Tokyo" timezone.)* |
|||
|
|||
This feature utilizes the `IClock` service, which provides methods like `ConvertToUserTime` and `ConvertToUtc` to facilitate timezone conversions. By configuring the `Abp.Timing.TimeZone` setting, developers can ensure a consistent and user-friendly experience across applications with a global user base. |
|||
> For a more detailed guide on implementing and using this feature, refer to the article: [Developing a Multi-Timezone Application Using the ABP Framework](https://abp.io/community/articles/developing-a-multitimezone-application-using-the-abp-framework-zk7fnrdq). It offers step-by-step instructions and examples for handling multi-timezone scenarios in ABP applications. |
|||
|
|||
### Identity Pro Module: Require Email Verification to Register |
|||
|
|||
[ABP Identity Pro module](https://abp.io/docs/9.2/modules/identity-pro) has been enhanced with a new feature that allows administrators to require email verification during the registration process. This security improvement ensures that users must verify their email addresses before their registration is considered complete. Enabling this feature is especially important for applications that want to prevent spam registrations. |
|||
|
|||
Administrators can enable or disable this feature through the **Identity management -> Identity Verification (tab)** settings page (by checking the `Enforce email verification to register` checkbox): |
|||
|
|||
 |
|||
|
|||
### Switching users during OAuth login |
|||
|
|||
If you have an OAuth/Auth Server application using the [ABP Account Pro module](https://abp.io/docs/9.2/modules/account-pro) , you can pass the `prompt=select_account` parameter to force the user to select an account. |
|||
|
|||
 |
|||
|
|||
For more information, please refer to the [Switching users during OAuth login](https://abp.io/docs/9.2/modules/account-pro#switching-users-during-oauth-login) documentation. |
|||
|
|||
### New ABP Community Articles |
|||
|
|||
There are exciting articles contributed by the ABP community as always. I will highlight some of them here: |
|||
|
|||
* [Implementing CQRS with MediatR in ABP](https://abp.io/community/articles/implementing-cqrs-with-mediatr-in-abp-xiqz2iio) by [Engincan Veske](https://github.com/EngincanV) |
|||
* [Using Vue Components in a Razor Pages ABP Application](https://abp.io/community/articles/using-vue-components-in-a-razor-pages-abp-application-z3jr07tv) by [Enis Necipoglu](https://github.com/enisn) |
|||
* [Using ABP's AWS Blob Storing Provider with DigitalOcean Spaces](https://abp.io/community/articles/using-abps-aws-blob-storing-provider-with-digitalocean-spaces-7hlyb25g) by [Suhaib Mousa](https://abp.io/community/members/suhaib-mousa) |
|||
* [Video Post: Using Vue Components in a Razor Pages ABP Application](https://abp.io/community/articles/using-vue-components-in-a-razor-pages-abp-application-z3jr07tv) by [Enis Necipoglu](https://github.com/enisn) |
|||
* [Understanding the Embedded Files in ABP Framework](https://abp.io/community/articles/understanding-the-embedded-files-in-abp-framework-nsrp8aa9) by [Liming Ma](https://github.com/maliming) |
|||
* [How to Change the CurrentUser in ABP?](https://abp.io/community/articles/how-to-change-the-currentuser-in-abp-i3uu1m7g) by [Engincan Veske](https://github.com/EngincanV) |
|||
|
|||
|
|||
Thanks to the ABP Community for all the content they have published. You can also [post your ABP-related (text or video) content](https://abp.io/community/posts/create) to the ABP Community. |
|||
|
|||
## Conclusion |
|||
|
|||
This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://abp.io/docs/9.2/release-info/road-map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v9.2 RC and provide feedback to help us release a more stable version. |
|||
|
|||
Thanks for being a part of this community! |
|||
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 727 KiB |
|
After Width: | Height: | Size: 34 KiB |
@ -0,0 +1,38 @@ |
|||
We are excited to introduce the ABP Contributor Program, which is an amazing opportunity for community enthusiasts to contribute to the ABP ecosystem while getting valuable benefits\! |
|||
|
|||
**Create, Contribute and Be Part of Something Bigger** |
|||
|
|||
This is your opportunity to not only share your knowledge but also to grow alongside a passionate community of developers and creators. When you create and contribute to the ABP ecosystem, you’re not just building content, you’re also helping shape the future of ABP. |
|||
|
|||
## **Why Become an ABP Contributor?** |
|||
|
|||
By sharing your knowledge, creating valuable content, and engaging with the ABP Community, you’ll enjoy a range of benefits, including: |
|||
|
|||
* Free ABP Personal License – If you meet all the requirements, you’ll receive an ABP Personal License at no cost |
|||
|
|||
* Community Badge & Title – Stand out in the ABP Community with a special contributor badge and label next to your name. |
|||
|
|||
* Exclusive Discord Role – Get a unique Contributor role on the ABP Community Discord server. |
|||
|
|||
* Increased Visibility – Your contributions will be recognized by hundreds of thousands of developers worldwide. |
|||
|
|||
Completely Free – No fees, just log in, contribute, and start collecting benefits\! |
|||
|
|||
## **How to Apply?** |
|||
|
|||
If you’re passionate about ABP and think you meet the required criteria, follow these simple steps: |
|||
|
|||
Create content related to ABP: blog posts, tutorials, videos, or documentation. You can publish it on the ABP Community website or any external platform (just make sure to submit it to the ABP Community site). |
|||
|
|||
Apply by email: Send your application to marketing@volosoft.com, and the ABP Team will review your eligibility. |
|||
|
|||
Periodic Assessments: The team reviews submissions every three months to determine and renew contributor status. Keep contributing to continue enjoying the perks\! |
|||
|
|||
## **Join Us & Grow\!** |
|||
|
|||
This is your chance to give back to the community, grow your presence, and get rewarded for your contributions. Whether you love writing documentation, sharing tutorials, or building open-source projects, your efforts matter. |
|||
|
|||
Apply today and be part of something impactful\! |
|||
|
|||
|
|||
[image1]: images/img_1.png |
|||
@ -0,0 +1,170 @@ |
|||
# Common Errors in JWT Bearer Authentication |
|||
|
|||
When implementing JWT Bearer authentication in an ABP(tiered) application, you might occasionally encounter errors starting with `IDX`. These errors are related to JWT Bearer Token validation and this article will help you understand and resolve them. |
|||
|
|||
## Enable JWT Bearer authentication |
|||
|
|||
Your API project usually contains the following code, which enables JWT Bearer authentication and makes it as the default authentication scheme. |
|||
|
|||
We simply configure the JWT's `Authority` and `Audience` properties, and it will work fine. |
|||
|
|||
```csharp |
|||
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) |
|||
.AddJwtBearer(options => |
|||
{ |
|||
options.Authority = "https://localhost:44301/"; //configuration["AuthServer:Authority"]; |
|||
options.Audience = "MyProjectName"; |
|||
}); |
|||
``` |
|||
|
|||
> `AddJwtBearer` and `AddAbpJwtBearer` will do the same thing, but `AddAbpJwtBearer` is recommended. |
|||
|
|||
## JWT authentication process |
|||
|
|||
Let's take a look at how the above code works. |
|||
|
|||
A JWT Token usually consists of three parts: `Header`, `Payload`, and `Signature`. |
|||
|
|||
- `Header`: Contains the type and signing algorithm of the token |
|||
- `Payload`: Contains the claims of the token, including `sub`, `aud`, `exp`, `iat`, `iss`, `jti`, `preferred_username`, `given_name`, `role`, `email`, etc. |
|||
- `Signature`: The cryptographic signature of the token used to verify its authenticity |
|||
|
|||
Here is an example of a JWT Token issued by `AuthServer(OpenIddict)`: |
|||
|
|||
The `Header` part: |
|||
|
|||
 |
|||
|
|||
The `Payload` part: |
|||
|
|||
 |
|||
|
|||
### TokenValidationParameters |
|||
|
|||
In the `JwtBearerOptions`, there is a `TokenValidationParameters` property, which is used to validate the JWT Token. |
|||
|
|||
The default implementation for JWT Token validation is `JsonWebTokenHandler`, which comes from the [Microsoft.IdentityModel.JsonWebTokens](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/) package. |
|||
|
|||
We didn't set the `TokenValidationParameters` property in the code above, so the default values below will be used: |
|||
|
|||
```csharp |
|||
//... |
|||
TokenValidationParameters.ValidateAudience = true |
|||
TokenValidationParameters.ValidAudience = "MyProjectName" |
|||
TokenValidationParameters.ValidAudiences = null |
|||
|
|||
TokenValidationParameters.ValidateIssuer = true |
|||
TokenValidationParameters.ValidIssuer = null |
|||
TokenValidationParameters.ValidIssuers = null |
|||
//... |
|||
``` |
|||
|
|||
### JWT Bearer Token Validation Process |
|||
|
|||
During JWT Bearer authentication, API website will get the token from the HTTP request and validate it. |
|||
|
|||
The `JsonWebTokenHandler` will get the `OpenID Connect` metadata from the `AuthServer`, it will be used in the validation process, the current metadata request address is: https://localhost:44301/.well-known/openid-configuration , it is a fixed address calculated from the `Authority` property. |
|||
|
|||
First, the token's Signature is verified using the public key obtained from `OpenID Connect` metadata(https://localhost:44301/.well-known/jwks). |
|||
|
|||
Then, the payload is validated. The payload is a JSON object containing essential information such as the `token type`, `expiration time`, `issuer`, and `audience` etc. |
|||
|
|||
Most of the validation problems we may encounter are payload validation failures, for example: |
|||
|
|||
#### Lifetime |
|||
|
|||
If the token in your request has expired, the validation will fail. You will see the exception information like `IDX10230` in the log. |
|||
|
|||
#### Audience |
|||
|
|||
The `ValidAudience` of `TokenValidationParameters` is `MyProjectName`, the `aud` in the payload of the token is also `MyProjectName`, if the token does not contain `aud` or the `aud` does not match, the validation will fail. You may see the exception information like `IDX10206`, `IDX10277` or `IDX10208`. |
|||
|
|||
> If the `ValidateAudience` of `TokenValidationParameters` is `false`, then the `aud` will not be validated. |
|||
|
|||
#### Issuer |
|||
|
|||
The default value of `TokenValidationParameters.ValidateIssuer` is `true`, it requires the token's payload to contain the `issuer` field, and it must match one of `TokenValidationParameters.ValidIssuer` or `TokenValidationParameters.ValidIssuers`. |
|||
|
|||
> The default value of `ValidIssuer` or `ValidIssuers` is `null`, it will use the `issuer` from the `OpenID Connect` metadata as the default value. |
|||
|
|||
1. If the token's payload does not contain the `issuer` field, you may see the error `IDX10211`. |
|||
2. If the API website cannot get the `OpenID Connect` metadata from AuthServer website, the validation will fail. You may see the error `IDX10204`, the full exception message is: `IDX10204: Unable to validate issuer. validationParameters.ValidIssuer is null or whitespace AND validationParameters.ValidIssuers is null or empty.` |
|||
3. If the `issuer` does not match, the validation will fail. You may see the error `IDX10205` in the log. |
|||
|
|||
> If the `ValidateIssuer` of `TokenValidationParameters` is `false`, then the `issuer` will not be validated. |
|||
|
|||
> Please note that `OpenIddict` will use the current HTTP request information as the value of `issuer`. If the AuthServer website is deployed behind a reverse proxy or similar deployment configurations, the `issuer` in the token may not be the value you expect. In this case, please specify it manually. |
|||
|
|||
```csharp |
|||
PreConfigure<OpenIddictServerBuilder>(serverBuilder => |
|||
{ |
|||
serverBuilder.SetIssuer("https://localhost:44301/"); |
|||
}); |
|||
``` |
|||
|
|||
## Troubleshooting |
|||
|
|||
To troubleshoot any `IDX` errors during JWT authentication, you can enable detailed logging by configuring the `identitymodel` logs as follows: |
|||
|
|||
```csharp |
|||
using System.Diagnostics.Tracing; |
|||
using Microsoft.IdentityModel.Logging; |
|||
|
|||
public class Program |
|||
{ |
|||
public async static Task<int> Main(string[] args) |
|||
{ |
|||
IdentityModelEventSource.ShowPII = true; |
|||
IdentityModelEventSource.Logger.LogLevel = EventLevel.Verbose; |
|||
var wilsonTextLogger = newTextWriterEventListener("Logs/identitymodel.txt"); |
|||
wilsonTextLogger.EnableEvents(IdentityModelEventSource.Logger, EventLevel.Verbose); |
|||
|
|||
//... |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Additionally, you can enable `OpenIddict`'s `Verbose` logs for more detailed debugging information: |
|||
|
|||
```csharp |
|||
var loggerConfiguration = new LoggerConfiguration() |
|||
.MinimumLevel.Debug() |
|||
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning) |
|||
.MinimumLevel.Override("OpenIddict", LogEventLevel.Verbose) |
|||
.Enrich.FromLogContext() |
|||
.WriteTo.Async(c => c.File("Logs/logs.txt")) |
|||
``` |
|||
|
|||
## Summary |
|||
|
|||
For JWT authentication, you need to pay attention to the following key points: |
|||
|
|||
1. Ensure your API website can communicate with the AuthServer properly |
|||
2. Verify that the `aud` claim in your token matches the expected audience |
|||
3. Confirm that the `issuer` claim in your token is valid and matches the configuration |
|||
|
|||
You can customize the `JwtBearerOptions`'s `TokenValidationParameters` to modify the validation rules to meet your actual needs. |
|||
|
|||
For example, if your `issuer` needs to support multiple subdomains, you can use the [Owl.TokenWildcardIssuerValidator](https://github.com/maliming/Owl.TokenWildcardIssuerValidator) library to customize the validation. |
|||
|
|||
```csharp |
|||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) |
|||
.AddJwtBearer(options => |
|||
{ |
|||
options.Authority = "https://abp.io"; |
|||
options.Audience = "abp_io"; |
|||
|
|||
options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator; |
|||
options.TokenValidationParameters.ValidIssuers = new[] |
|||
{ |
|||
"https://{0}.abp.io" |
|||
}; |
|||
}); |
|||
``` |
|||
|
|||
## References |
|||
|
|||
- [Configure JWT bearer authentication in ASP.NET Core]([https://learn.microsoft.com/en-us/aspnet/core/security/authentication/jwt-auth?view=aspnetcore-8.0](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-jwt-bearer-authentication)) |
|||
- [OpenIddict](https://github.com/openiddict/openiddict-core) |
|||
- [IdentityModel](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) |
|||
- [Owl.TokenWildcardIssuerValidator](https://github.com/maliming/Owl.TokenWildcardIssuerValidator) |
|||
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 142 KiB |
@ -0,0 +1,262 @@ |
|||
# Using Microsoft AI Extensions Library and OpenAI to Summarize User Comments |
|||
|
|||
Either you are building an e-commerce application or a simple blog, **user comments** (about your products or blog posts) **can grow rapidly**, making it harder for users to get the gist of discussions at a glance. AI is a pretty good tool to solve the problem. By using AI, you can **summarize all the user comments** and show a single paragraph to your users, so they can easily understand the overall thought of users about the product or the blog post. |
|||
|
|||
In this tutorial, we’ll walk through a real-life implementation of using AI to summarize multiple user comments in an application. I will implement the solution based on ABP's **[CMS Kit](https://abp.io/docs/latest/modules/cms-kit)** library, as it already features a **[commenting system](https://abp.io/docs/latest/modules/cms-kit/comments)** and a [demo application](https://cms-kit-demo.abpdemo.com/) that displays user comments on **[gallery images](https://cms-kit-demo.abpdemo.com/image-gallery)** (it has not a comment summary feature yet, we will implement it in this tutorial). |
|||
|
|||
## A Screenshot |
|||
|
|||
Here, an example screenshot from the application with the comment summary feature: |
|||
|
|||
 |
|||
|
|||
## Cloning the Repository |
|||
|
|||
If you want to follow the development, you can clone the [CMS Kit Demo repository](https://github.com/abpframework/cms-kit-demo) to your computer and make it running by following the instructions on the [README file](https://github.com/abpframework/cms-kit-demo?tab=readme-ov-file#cms-kit-demo). |
|||
|
|||
I suggest to you to play a little with [the application](https://cms-kit-demo.abpdemo.com/) (create a new user for yourself, add some comments to the images in the gallery), so you understand how it works. |
|||
|
|||
## Preparing the Solution for AI |
|||
|
|||
I will use [Microsoft AI Extensions Library](https://learn.microsoft.com/en-us/dotnet/ai/ai-extensions) to use the AI features. It is an abstraction library that can work with multiple AI models and tools. I will use an OpenAI model in the demo. |
|||
|
|||
The first step is to add the [Microsoft.Extensions.AI.OpenAI](http://nuget.org/packages/Microsoft.Extensions.AI.OpenAI) NuGet package to the project: |
|||
|
|||
````bash |
|||
dotnet add package Microsoft.Extensions.AI.OpenAI --prerelease |
|||
```` |
|||
|
|||
>The Microsoft AI Extensions Library was in preview at the time when I wrote this article. If it has a stable release now, you can remove the `--prerelease` parameter for the preceding command. |
|||
|
|||
We will store the OpenAI key and model name in user secrets. So, locate the root path of the CMS Kit project (`src\CmsKitDemo` folder) and execute the following commands in order in a command-line terminal: |
|||
|
|||
````bash |
|||
dotnet user-secrets init |
|||
dotnet user-secrets set OpenAIKey <your-openai-key> |
|||
dotnet user-secrets set ModelName <your-openai-model-name> |
|||
```` |
|||
|
|||
For this example, you need to have an [OpenAI API Key](https://platform.openai.com/). That's all. Now, we are ready to use the AI. |
|||
|
|||
## Implementing the AI Summarization |
|||
|
|||
Let's start from the most important point of this article: Comment summarization. I will create a class named `AiCommentSummarizer` to implement the summarization work. Here, the full content of that class: |
|||
|
|||
````csharp |
|||
using System.Text; |
|||
using Microsoft.Extensions.AI; |
|||
using OpenAI; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace CmsKitDemo.Utils; |
|||
|
|||
public class AiCommentSummarizer : ITransientDependency |
|||
{ |
|||
private readonly IConfiguration _configuration; |
|||
|
|||
public AiCommentSummarizer(IConfiguration configuration) |
|||
{ |
|||
_configuration = configuration; |
|||
} |
|||
|
|||
public async Task<string> SummarizeAsync(string[] commentTexts) |
|||
{ |
|||
// Get the model and key from the configuration |
|||
var aiModel = _configuration["ModelName"]; |
|||
var apiKey = _configuration["OpenAIKey"]; |
|||
|
|||
if (aiModel.IsNullOrEmpty() || apiKey.IsNullOrEmpty()) |
|||
{ |
|||
return ""; |
|||
} |
|||
|
|||
// Create the IChatClient |
|||
var client = new OpenAIClient(apiKey) |
|||
.GetChatClient(aiModel) |
|||
.AsIChatClient(); |
|||
|
|||
// Create a prompt (input for AI) |
|||
var promptBuilder = new StringBuilder(); |
|||
|
|||
promptBuilder.AppendLine( |
|||
@"There are comments from different users of our website about an image. |
|||
We want to summarize the comments into a single comment. |
|||
Return a single comment with a maximum of 512 characters. Comments are separated by a newline character and given below." |
|||
); |
|||
promptBuilder.AppendLine(); |
|||
|
|||
foreach (var commentText in commentTexts) |
|||
{ |
|||
promptBuilder.AppendLine("User comment:"); |
|||
promptBuilder.AppendLine(commentText); |
|||
promptBuilder.AppendLine(); |
|||
} |
|||
|
|||
// Submit the prompt and get the response |
|||
var response = await client.GetResponseAsync( |
|||
promptBuilder.ToString(), |
|||
new ChatOptions { MaxOutputTokens = 1024 } |
|||
); |
|||
|
|||
return response.Text; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
That class is pretty simple and already decorated with comments: |
|||
|
|||
* First, we are getting the API Key and an OpenAI model name from user secrets. I used `gpt-4.1` as the model name, but you can use another available model. |
|||
* Then we are obtaining an `IChatClient` reference for OpenAI. `IChatClient` interface is an abstraction that is provided by the [Microsoft AI Extensions Library](https://learn.microsoft.com/en-us/dotnet/ai/ai-extensions) library, so we can implement rest of the code independently from OpenAI. |
|||
* Then we continue by building a proper prompt (input) for the AI operation. |
|||
* And finally we are using the AI to generate a response (the summary). |
|||
|
|||
At this point, all the AI-related work has already been done. The rest of this article explains how to integrate that summarization feature with the [CMS Kit Demo application](https://cms-kit-demo.abpdemo.com/). |
|||
|
|||
## Adding a CommentsSummary Property to the GalleryImage Entity |
|||
|
|||
The `GalleryImage` entity is used to represent an image on [the image gallery](https://cms-kit-demo.abpdemo.com/image-gallery). I add a `CommentsSummary` property to that entity: |
|||
|
|||
````csharp |
|||
public class GalleryImage : CreationAuditedAggregateRoot<Guid> |
|||
{ |
|||
public string Description { get; set; } |
|||
|
|||
public Guid CoverImageMediaId { get; set; } |
|||
|
|||
public string CommentsSummary { get; set; } // The new property is here |
|||
|
|||
//... |
|||
} |
|||
```` |
|||
|
|||
Since the CMS Kit Demo application uses Entity Framework Core, I need to add a new database schema migration and update the database: |
|||
|
|||
````bash |
|||
dotnet ef migrations add Added_Summary_To_GalleryImage |
|||
dotnet ef database update |
|||
```` |
|||
|
|||
## Updating the Summary |
|||
|
|||
Great, we have a `GalleryImage.CommentsSummary` property now. But, how will it be updated when a users adds or removes a comment for an image? To implement that; |
|||
|
|||
* We will listen all the change events for user comments (when a user adds, removes or updates a comment). |
|||
* Whenever a comment is changed, we will find the related gallery image, retrieve all the user comments for this image, use the `AiCommentSummarizer` class to summarize all the comments. |
|||
* Finally, we wil set the `GalleryImage.CommentsSummary` property with the generated summary text. |
|||
|
|||
Here, the implementation: |
|||
|
|||
````csharp |
|||
using CmsKitDemo.Entities; |
|||
using CmsKitDemo.Utils; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Entities.Events; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.EventBus; |
|||
using Volo.CmsKit.Comments; |
|||
|
|||
namespace CmsKitDemo.EventHandlers; |
|||
|
|||
public class GalleryImageCommentListener : |
|||
ILocalEventHandler<EntityChangedEventData<Comment>>, |
|||
ITransientDependency |
|||
{ |
|||
private readonly IRepository<GalleryImage, Guid> _galleryImageRepository; |
|||
private readonly IRepository<Comment, Guid> _commentRepository; |
|||
private readonly AiCommentSummarizer _aiCommentSummarizer; |
|||
|
|||
public GalleryImageCommentListener( |
|||
IRepository<GalleryImage, Guid> galleryImageRepository, |
|||
IRepository<Comment, Guid> commentRepository, |
|||
AiCommentSummarizer aiCommentSummarizer) |
|||
{ |
|||
_galleryImageRepository = galleryImageRepository; |
|||
_commentRepository = commentRepository; |
|||
_aiCommentSummarizer = aiCommentSummarizer; |
|||
} |
|||
|
|||
public async Task HandleEventAsync(EntityChangedEventData<Comment> eventData) |
|||
{ |
|||
var comment = eventData.Entity; |
|||
|
|||
//Here, we only interest in comments related to image gallery items |
|||
if (comment.EntityType != CmsKitDemoConsts.ImageGalleryEntityType) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (!Guid.TryParse(comment.EntityId, out var galleryImageId)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Get the related image from database |
|||
var galleryImage = await _galleryImageRepository.FindAsync(galleryImageId); |
|||
if (galleryImage == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Get all the comments related to the image |
|||
var queryable = await _commentRepository.GetQueryableAsync(); |
|||
var allCommentTexts = await queryable |
|||
.Where(c => c.EntityType == CmsKitDemoConsts.ImageGalleryEntityType && |
|||
c.EntityId == comment.EntityId) |
|||
.Select(c => c.Text) |
|||
.ToArrayAsync(); |
|||
|
|||
// Update the summary of comments related to the image |
|||
if (allCommentTexts.Length <= 0) |
|||
{ |
|||
galleryImage.CommentsSummary = ""; |
|||
} |
|||
else |
|||
{ |
|||
galleryImage.CommentsSummary = |
|||
await _aiCommentSummarizer.SummarizeAsync(allCommentTexts); |
|||
} |
|||
|
|||
// Update the image in database |
|||
await _galleryImageRepository.UpdateAsync(galleryImage); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Let's explain that class: |
|||
|
|||
* `GalleryImageCommentListener` implements the `ILocalEventHandler<EntityChangedEventData<Comment>>` interface. In this way, it can handle an event whenever a `Comment` [entity](https://abp.io/docs/latest/framework/architecture/domain-driven-design/entities) is changed (created, updated or deleted). We are using ABP's [local event bus](https://abp.io/docs/latest/framework/infrastructure/event-bus/local) and its [pre-defined events](https://abp.io/docs/latest/framework/infrastructure/event-bus/local#pre-built-events). |
|||
* `HandleEventAsync` is called by the ABP Framework whenever a new `Comment` is created, or an existing `Comment` is deleted or updated. |
|||
* ABP's `Comment` entity is reusable and it can be associated with any kind of objects (blog posts, images, etc). So, first we are checking if this comment is related to an image gallery item. |
|||
* Then we are getting the related `GalleryImage` entity from the database. |
|||
* And getting all comments (including the new one) from the database for this image. |
|||
* Finally, using the `AiCommentSummarizer` class to generate the summary and set the `CommentsSummary` property. |
|||
|
|||
## Show the Summary Card on the UI |
|||
|
|||
Everything is ready on the backend. Now, we can show the summary text on the user interface. To do, that, I added `CommentsSummary` property also to the `GalleryImageDto` class and used it on the `/Pages/Gallery/Detail.cshtml` view: |
|||
|
|||
````csharp |
|||
@if (!Model.Image.CommentsSummary.IsNullOrEmpty()) |
|||
{ |
|||
<div class="card mt-3"> |
|||
<div class="card-body"> |
|||
<h6 class="card-title">Summary of the User Comments</h6> |
|||
<p class="mb-auto">@Model.Image.CommentsSummary</p> |
|||
</div> |
|||
</div> |
|||
} |
|||
```` |
|||
|
|||
That section renders the following card on the user interface: |
|||
|
|||
 |
|||
|
|||
## Conclusion |
|||
|
|||
In this article, I demonstrated how to use [Microsoft AI Extensions Library](https://learn.microsoft.com/en-us/dotnet/ai/ai-extensions) to work with OpenAI for summarization of multiple user comments. I reused the [ABP's CMS Kit Demo application](https://github.com/abpframework/cms-kit-demo) to show it in a more real world example. |
|||
|
|||
## Source Code |
|||
|
|||
* [Source code of the CMS Kit Demo application](https://github.com/abpframework/cms-kit-demo) |
|||
* [All the changes made for this article (as a pull request)](https://github.com/abpframework/cms-kit-demo/pull/18) |
|||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,134 @@ |
|||
# Integrating .NET AI Chat Template with ABP Framework |
|||
|
|||
This article demonstrates how to integrate the [.NET AI Chat Template](https://devblogs.microsoft.com/dotnet/announcing-dotnet-ai-template-preview2/) into an ABP Framework application, enabling powerful AI chat capabilities in your ABP-based solution. |
|||
|
|||
 |
|||
|
|||
## Step 1: Create a New ABP Project |
|||
|
|||
First, let's create a new [single-layer Blazor Server project](https://abp.io/docs/latest/solution-templates/single-layer-web-application/overview) named `AbpAiChat` using ABP Studio, You can also use the following ABP CLI command to create the project: |
|||
|
|||
```bash |
|||
abp new AbpAiChat -t app-nolayers --ui-framework blazor-server --use-open-source-template |
|||
``` |
|||
|
|||
## Step 2: Integrate AI Chat Template |
|||
|
|||
The integration process involves copying and adapting the .NET AI Chat Template code into our ABP project. The template code is already included in our sample project, so you don't need to install it separately. |
|||
|
|||
### 2.1 Project Structure Changes |
|||
|
|||
1. Copy Blazor components to the `Components` folder |
|||
2. Copy AI service classes to the `Services` folder |
|||
3. Add required entities(`IngestedDocument`, `IngestedRecord`) to the `AbpAiChatDbContext` and add new migration |
|||
4. Copy frontend resources to the `wwwroot` folder |
|||
5. Adjust some styles to capatible with the ABP theme |
|||
|
|||
### 2.2 Required NuGet Packages |
|||
|
|||
Add the following packages to `AbpAiChat.csproj`: |
|||
|
|||
```xml |
|||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.4.3-preview.1.25230.7" /> |
|||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" /> |
|||
<PackageReference Include="Microsoft.Extensions.AI" Version="9.4.3-preview.1.25230.7" /> |
|||
<PackageReference Include="Microsoft.SemanticKernel.Core" Version="1.47.0" /> |
|||
<PackageReference Include="PdfPig" Version="0.1.9" /> |
|||
<PackageReference Include="System.Linq.Async" Version="6.0.1" /> |
|||
``` |
|||
|
|||
### 2.3 Configure AI Services |
|||
|
|||
Add the following configuration to `AbpAiChatModule.cs`: |
|||
|
|||
```csharp |
|||
private void ConfigureAi(ServiceConfigurationContext context) |
|||
{ |
|||
var credential = new ApiKeyCredential(context.Services.GetConfiguration()["GitHubToken"] ?? throw new InvalidOperationException("Missing configuration: GitHubToken. See the README for details.")); |
|||
var openAiOptions = new OpenAIClientOptions() |
|||
{ |
|||
Endpoint = new Uri("https://models.inference.ai.azure.com") |
|||
}; |
|||
|
|||
var ghModelsClient = new OpenAIClient(credential, openAiOptions); |
|||
var chatClient = ghModelsClient.GetChatClient("gpt-4o-mini").AsIChatClient(); |
|||
var embeddingGenerator = ghModelsClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); |
|||
|
|||
var vectorStore = new JsonVectorStore(Path.Combine(AppContext.BaseDirectory, "vector-store")); |
|||
|
|||
context.Services.AddSingleton<IVectorStore>(vectorStore); |
|||
context.Services.AddScoped<DataIngestor>(); |
|||
context.Services.AddSingleton<SemanticSearch>(); |
|||
context.Services.AddChatClient(chatClient).UseFunctionInvocation().UseLogging(); |
|||
context.Services.AddEmbeddingGenerator(embeddingGenerator); |
|||
|
|||
context.Services.Configure<AbpAspNetCoreContentOptions>(options => |
|||
{ |
|||
options.ContentTypeMaps.Add(".mjs", "application/javascript"); |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
The `ConfigureAi` method is called in the `ConfigureServices` method of `AbpAiChatModule`. It sets up the AI services, including the OpenAI client, chat client, embedding generator, and vector store. |
|||
|
|||
### 2.4 Configure GitHub Token |
|||
|
|||
Add your GitHub Personal Access Token to `appsettings.json`: |
|||
|
|||
```json |
|||
{ |
|||
"GitHubToken": "your-github-token" |
|||
} |
|||
``` |
|||
|
|||
You can obtain your token from [GitHub Personal Access Tokens](https://github.com/settings/personal-access-tokens). |
|||
|
|||
## Step 3: Add Custom AI Functionality |
|||
|
|||
Let's add a custom AI function to retrieve the current user's information. Update the `Chat.razor` component: |
|||
|
|||
```csharp |
|||
chatOptions.Tools = |
|||
[ |
|||
AIFunctionFactory.Create(SearchAsync), |
|||
AIFunctionFactory.Create(GetWeather), |
|||
AIFunctionFactory.Create(GetCurrentUserInfo) |
|||
]; |
|||
|
|||
[Description("Get current user information")] |
|||
private Task<string> GetCurrentUserInfo() |
|||
{ |
|||
return Task.FromResult(CurrentUser.IsAuthenticated ? |
|||
$"UserId: {CurrentUser.Id}, Name: {CurrentUser.UserName}, Email: {CurrentUser.Email}, Roles: {string.Join(", ", CurrentUser.Roles)}" : |
|||
"No user information available."); |
|||
} |
|||
``` |
|||
|
|||
## Step 4: Add Navigation |
|||
|
|||
Add a `Chat` menu item in `AbpAiChatMenuContributor` to navigate to the AI Chat component. |
|||
|
|||
## Running the Application |
|||
|
|||
After completing the integration, you can run the application and access the AI chat functionality. The chat interface allows you to: |
|||
|
|||
- Get weather information |
|||
- Ask questions about PDF content |
|||
- Retrieve current user information |
|||
- And more! |
|||
|
|||
 |
|||
|
|||
## Conclusion |
|||
|
|||
This integration demonstrates how to leverage the power of AI in your ABP Framework applications. The .NET AI Chat Template provides a solid foundation for building intelligent chat interfaces, and ABP Framework makes it more powerful. |
|||
|
|||
## References |
|||
|
|||
- [ABP Single Layer Solution](https://abp.io/docs/latest/solution-templates/single-layer-web-application/overview) |
|||
- [.NET AI Template Documentation](https://devblogs.microsoft.com/dotnet/announcing-dotnet-ai-template-preview1/) |
|||
- [GitHub Personal Access Tokens Guide](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) |
|||
|
|||
## Source Code |
|||
|
|||
- [AbpAiChat Source Code](https://github.com/abpframework/abp-samples/tree/master/AIChat) |
|||
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
@ -0,0 +1,268 @@ |
|||
# Resolving Tenant from Route in ABP Framework |
|||
|
|||
The ABP Framework provides multi-tenancy support with various ways to resolve tenant information, including: Cookie, Header, Domain, Route, and more. |
|||
|
|||
This article will demonstrate how to resolve tenant information from the route. |
|||
|
|||
## Tenant Information in Routes |
|||
|
|||
In the ABP Framework, tenant information in routes is handled by the `RouteTenantResolveContributor`. |
|||
|
|||
Let's say your application is hosted at `https://abp.io` and you have a tenant named `acme`. You can add the `{__tenant}` variable to your controller or page routes like this: |
|||
|
|||
```csharp |
|||
[Route("{__tenant}/[Controller]")] |
|||
public class MyController : MyProjectNameController |
|||
{ |
|||
[HttpGet] |
|||
public IActionResult Get() |
|||
{ |
|||
return Ok("Hello My Page"); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```cshtml |
|||
@page "{__tenant?}/mypage" |
|||
@model MyPageModel |
|||
|
|||
<html> |
|||
<body> |
|||
<h1>My Page</h1> |
|||
</body> |
|||
</html> |
|||
``` |
|||
|
|||
When you access `https://abp.io/acme/my` or `https://abp.io/acme/mypage`, ABP will automatically resolve the tenant information from the route. |
|||
|
|||
## Adding __tenant to Global Routes |
|||
|
|||
While we've shown how to add `{__tenant}` to individual controllers or pages, you might want to add it globally to your entire application. Here's how to implement this: |
|||
|
|||
```cs |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Microsoft.AspNetCore.Mvc.ApplicationModels; |
|||
|
|||
namespace MyCompanyName; |
|||
|
|||
public class AddTenantRouteToPages : IPageRouteModelConvention, IApplicationModelConvention |
|||
{ |
|||
public void Apply(PageRouteModel model) |
|||
{ |
|||
var selectorCount = model.Selectors.Count; |
|||
var selectorModels = new List<SelectorModel>(); |
|||
for (var i = 0; i < selectorCount; i++) |
|||
{ |
|||
var selector = model.Selectors[i]; |
|||
selectorModels.Add(new SelectorModel |
|||
{ |
|||
AttributeRouteModel = new AttributeRouteModel |
|||
{ |
|||
Template = AttributeRouteModel.CombineTemplates("{__tenant:regex(^[a-zA-Z0-9]+$)}", selector.AttributeRouteModel!.Template!.RemovePreFix("/")) |
|||
} |
|||
}); |
|||
} |
|||
foreach (var selectorModel in selectorModels) |
|||
{ |
|||
model.Selectors.Add(selectorModel); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public class AddTenantRouteToControllers :IApplicationModelConvention |
|||
{ |
|||
public void Apply(ApplicationModel application) |
|||
{ |
|||
var controllers = application.Controllers; |
|||
foreach (var controller in controllers) |
|||
{ |
|||
var selector = controller.Selectors.FirstOrDefault(); |
|||
if (selector == null || selector.AttributeRouteModel == null) |
|||
{ |
|||
controller.Selectors.Add(new SelectorModel |
|||
{ |
|||
AttributeRouteModel = new AttributeRouteModel |
|||
{ |
|||
Template = AttributeRouteModel.CombineTemplates("{__tenant:regex(^[[a-zA-Z0-9]]+$)}", controller.ControllerName) |
|||
} |
|||
}); |
|||
controller.Selectors.Add(new SelectorModel |
|||
{ |
|||
AttributeRouteModel = new AttributeRouteModel |
|||
{ |
|||
Template = controller.ControllerName |
|||
} |
|||
}); |
|||
} |
|||
else |
|||
{ |
|||
var template = selector.AttributeRouteModel?.Template; |
|||
template = template.IsNullOrWhiteSpace() ? "{__tenant:regex(^[[a-zA-Z0-9]]+$)}" : AttributeRouteModel.CombineTemplates("{__tenant:regex(^[[a-zA-Z0-9]]+$)}", template.RemovePreFix("/")); |
|||
controller.Selectors.Add(new SelectorModel |
|||
{ |
|||
AttributeRouteModel = new AttributeRouteModel |
|||
{ |
|||
Template = template |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Register the services: |
|||
|
|||
```cs |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//... |
|||
|
|||
PostConfigure<RazorPagesOptions>(options => |
|||
{ |
|||
options.Conventions.Add(new AddTenantRouteToPages()); |
|||
}); |
|||
|
|||
PostConfigure<MvcOptions>(options => |
|||
{ |
|||
options.Conventions.Add(new AddTenantRouteToControllers()); |
|||
}); |
|||
|
|||
// Configure cookie path to prevent authentication cookie loss |
|||
context.Services.ConfigureApplicationCookie(x => |
|||
{ |
|||
x.Cookie.Path = "/"; |
|||
}); |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
After implementing this, you'll notice that all controllers in your Swagger UI will have the `{__tenant}` route added: |
|||
|
|||
 |
|||
|
|||
## Handling Navigation Links |
|||
|
|||
To ensure navigation links automatically include tenant information, we need to add middleware that dynamically adds the tenant to the PathBase: |
|||
|
|||
```cs |
|||
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
|||
{ |
|||
//... |
|||
app.Use(async (httpContext, next) => |
|||
{ |
|||
var tenantMatch = Regex.Match(httpContext.Request.Path, "^/([^/.]+)(?:/.*)?$"); |
|||
if (tenantMatch.Groups.Count > 1 && !string.IsNullOrEmpty(tenantMatch.Groups[1].Value)) |
|||
{ |
|||
var tenantName = tenantMatch.Groups[1].Value; |
|||
if (!tenantName.IsNullOrWhiteSpace()) |
|||
{ |
|||
var tenantStore = httpContext.RequestServices.GetRequiredService<ITenantStore>(); |
|||
var tenantNormalizer = httpContext.RequestServices.GetRequiredService<ITenantNormalizer>(); |
|||
var tenantInfo = await tenantStore.FindAsync(tenantNormalizer.NormalizeName(tenantName)!); |
|||
if (tenantInfo != null) |
|||
{ |
|||
if (httpContext.Request.Path.StartsWithSegments(new PathString(tenantName.EnsureStartsWith('/')), out var matchedPath, out var remainingPath)) |
|||
{ |
|||
var originalPath = httpContext.Request.Path; |
|||
var originalPathBase = httpContext.Request.PathBase; |
|||
httpContext.Request.Path = remainingPath; |
|||
httpContext.Request.PathBase = originalPathBase.Add(matchedPath); |
|||
try |
|||
{ |
|||
await next(httpContext); |
|||
} |
|||
finally |
|||
{ |
|||
httpContext.Request.Path = originalPath; |
|||
httpContext.Request.PathBase = originalPathBase; |
|||
} |
|||
return; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
await next(httpContext); |
|||
}); |
|||
app.UseRouting(); |
|||
app.MapAbpStaticAssets(); |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
 |
|||
|
|||
After setting the PathBase, we need to add a custom tenant resolver to extract tenant information from the `PathBase`: |
|||
|
|||
```cs |
|||
public class MyRouteTenantResolveContributor : RouteTenantResolveContributor |
|||
{ |
|||
public const string ContributorName = "MyRoute"; |
|||
|
|||
public override string Name => ContributorName; |
|||
|
|||
protected override Task<string?> GetTenantIdOrNameFromHttpContextOrNullAsync(ITenantResolveContext context, HttpContext httpContext) |
|||
{ |
|||
var tenantId = httpContext.GetRouteValue(context.GetAbpAspNetCoreMultiTenancyOptions().TenantKey) ?? httpContext.Request.PathBase.ToString(); |
|||
var tenantIdStr = tenantId?.ToString()?.RemovePreFix("/"); |
|||
return Task.FromResult(!tenantIdStr.IsNullOrWhiteSpace() ? Convert.ToString(tenantIdStr) : null); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Register the MyRouteTenantResolveContributor with the ABP Framework: |
|||
|
|||
```cs |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//... |
|||
Configure<AbpTenantResolveOptions>(options => |
|||
{ |
|||
options.TenantResolvers.Add(new MyRouteTenantResolveContributor()); |
|||
}); |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
### Modifying abp.appPath |
|||
|
|||
```csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//... |
|||
context.Services.AddOptions<AbpThemingOptions>().Configure<IServiceProvider>((options, rootServiceProvider) => |
|||
{ |
|||
var currentTenant = rootServiceProvider.GetRequiredService<ICurrentTenant>(); |
|||
if (!currentTenant.Name.IsNullOrWhiteSpace()) |
|||
{ |
|||
options.BaseUrl = currentTenant.Name.EnsureStartsWith('/').EnsureEndsWith('/'); |
|||
} |
|||
}); |
|||
|
|||
context.Services.RemoveAll(x => x.ServiceType == typeof(IOptions<AbpThemingOptions>)); |
|||
context.Services.Add(ServiceDescriptor.Scoped(typeof(IOptions<>), typeof(OptionsManager<>))); |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
Browser console output: |
|||
|
|||
```cs |
|||
> https://localhost:44303/acme/ |
|||
> abp.appPath |
|||
> '/acme/' |
|||
``` |
|||
|
|||
## Summary |
|||
|
|||
By following these steps, you can implement tenant resolution from routes in the ABP Framework and handle navigation links appropriately. This approach provides a clean and maintainable way to manage multi-tenancy in your application. |
|||
|
|||
|
|||
## References |
|||
|
|||
- [ABP Multi-Tenancy](https://docs.abp.io/en/abp/latest/Multi-Tenancy) |
|||
- [Routing in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing) |
|||
- [HTML base tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) |
|||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 329 KiB |
|
After Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.5 MiB |
@ -0,0 +1,23 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Components.Web.Configuration; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.MauiBlazor; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class MauiCurrentApplicationConfigurationCacheResetService : |
|||
ICurrentApplicationConfigurationCacheResetService, |
|||
ITransientDependency |
|||
{ |
|||
private readonly MauiBlazorCachedApplicationConfigurationClient _mauiBlazorCachedApplicationConfigurationClient; |
|||
|
|||
public MauiCurrentApplicationConfigurationCacheResetService(MauiBlazorCachedApplicationConfigurationClient mauiBlazorCachedApplicationConfigurationClient) |
|||
{ |
|||
_mauiBlazorCachedApplicationConfigurationClient = mauiBlazorCachedApplicationConfigurationClient; |
|||
} |
|||
|
|||
public async Task ResetAsync() |
|||
{ |
|||
await _mauiBlazorCachedApplicationConfigurationClient.InitializeAsync(); |
|||
} |
|||
} |
|||