@ -0,0 +1,86 @@ |
|||||
|
# ABP.IO Platform 9.2 Final Has Been Released! |
||||
|
|
||||
|
We are glad to announce that [ABP](https://abp.io/) 9.2 stable version has been released today. |
||||
|
|
||||
|
## What's New With Version 9.2? |
||||
|
|
||||
|
All the new features were explained in detail in the [9.2 RC Announcement Post](https://abp.io/community/articles/abp-platform-9.2-rc-has-been-released-jpq072nh), so there is no need to review them again. You can check it out for more details. |
||||
|
|
||||
|
## Getting Started with 9.2 |
||||
|
|
||||
|
### Creating New Solutions |
||||
|
|
||||
|
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) to create new solutions. |
||||
|
|
||||
|
### How to Upgrade an Existing Solution |
||||
|
|
||||
|
You can upgrade your existing solutions with either ABP Studio or ABP CLI. In the following sections, both approaches are explained: |
||||
|
|
||||
|
### Upgrading via ABP Studio |
||||
|
|
||||
|
If you are already using the ABP Studio, you can upgrade it to the latest version. ABP Studio periodically checks for updates in the background, and when a new version of ABP Studio is available, you will be notified through a modal. Then, you can update it by confirming the opened modal. See [the documentation](https://abp.io/docs/latest/studio/installation#upgrading) for more info. |
||||
|
|
||||
|
After upgrading the ABP Studio, then you can open your solution in the application, and simply click the **Upgrade ABP Packages** action button to instantly upgrade your solution: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Upgrading via ABP CLI |
||||
|
|
||||
|
Alternatively, you can upgrade your existing solution via ABP CLI. First, you need to install the ABP CLI or upgrade it to the latest version. |
||||
|
|
||||
|
If you haven't installed it yet, you can run the following command: |
||||
|
|
||||
|
```bash |
||||
|
dotnet tool install -g Volo.Abp.Studio.Cli |
||||
|
``` |
||||
|
|
||||
|
Or to update the existing CLI, you can run the following command: |
||||
|
|
||||
|
```bash |
||||
|
dotnet tool update -g Volo.Abp.Studio.Cli |
||||
|
``` |
||||
|
|
||||
|
After installing/updating the ABP CLI, you can use the [`update` command](https://abp.io/docs/latest/CLI#update) to update all the ABP related NuGet and NPM packages in your solution as follows: |
||||
|
|
||||
|
```bash |
||||
|
abp update |
||||
|
``` |
||||
|
|
||||
|
You can run this command in the root folder of your solution to update all ABP related packages. |
||||
|
|
||||
|
## Migration Guides |
||||
|
|
||||
|
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: [ABP Version 9.2 Migration Guide](https://abp.io/docs/9.2/release-info/migration-guides/abp-9-2) |
||||
|
|
||||
|
## Community News |
||||
|
|
||||
|
### New ABP Community Articles |
||||
|
|
||||
|
As always, exciting articles have been contributed by the ABP community. I will highlight some of them here: |
||||
|
|
||||
|
* [Liming Ma](https://github.com/maliming) has published 3 new articles: |
||||
|
* [Integrating .NET AI Chat Template with ABP Framework](https://abp.io/community/articles/integrating-.net-ai-chat-template-with-abp-framework-qavb5p2j) |
||||
|
* [Resolving Tenant from Route in ABP Framework](https://abp.io/community/articles/resolving-tenant-from-route-in-abp-framework-ah7oru97) |
||||
|
* [Common Errors in JWT Bearer Authentication](https://abp.io/community/articles/common-errors-in-jwt-bearer-authentication-4u3wrbs5) |
||||
|
* [Engincan Veske](https://engincanveske.substack.com/) has published 3 new articles: |
||||
|
* [Understanding HttpApi.Client Project & Remote Services in an ABP Based Application](https://abp.io/community/articles/http-api-client-and-remote-services-in-abp-based-application-xkknsp6m) |
||||
|
* [Using Elsa 3 with the ABP Framework: A Comprehensive Guide](https://abp.io/community/articles/using-elsa-3-workflow-with-abp-framework-usqk8afg) |
||||
|
* [Implementing Custom Tenant Logo Feature in ABP Framework: A Step-by-Step Guide](https://abp.io/community/articles/implementing-custom-tenant-logo-feature-in-abp-framework-a-stepbystep-guide-sba96ac9) |
||||
|
* [Berkan Şaşmaz](https://berkansasmaz.com/) has published 2 new articles: |
||||
|
* [Understanding the Domain and Application Layers in ABP Framework](https://abp.io/community/articles/understanding-the-domain-and-application-layers-in-abp-1fipc4x4) |
||||
|
* [How Do We Maintain Code Quality and Technical Debt in Our .NET Codebase?](https://abp.io/community/articles/how-do-we-maintain-code-quality-and-technical-debt-in-our-.net-codebase-z7glpya1) |
||||
|
* [Enis Necipoğlu](https://github.com/enisn) has published 2 new articles: |
||||
|
* [White Labeling in ABP Framework](https://abp.io/community/articles/white-labeling-in-abp-framework-5trwmrfm) by [Enis Necipoğlu](https://github.com/enisn) |
||||
|
* [You do it wrong! Customizing ABP Login Page Correctly](https://abp.io/community/articles/you-do-it-wrong-customizing-abp-login-page-correctly-bna7wzt5) |
||||
|
* [Ariful Islam](https://abp.io/community/members/arif) has published 2 new articles: |
||||
|
* [Multi-Workspace Management for ABP Applications](https://abp.io/community/articles/multiworkspace-management-for-abp-applications-eghgty3j) |
||||
|
* [Using Semantic Kernel in the ABP Framework](https://abp.io/community/articles/using-semantic-kernel-in-the-abp-framework-qo5cnuzs) |
||||
|
* [Guide to Add Custom Modules in ABP.IO App](https://abp.io/community/articles/guide-to-add-custom-modules-in-abp.io-app-sttetffa) by [Harsh Gupta](https://abp.io/community/members/harshgupta) |
||||
|
* [Debugging NuGet Packages in ABP.IO: A Complete Guide](https://abp.io/community/articles/debugging-nuget-packages-in-abp.io-a-complete-guide-h13y2033) by [Suhaib Mousa](https://suhaibmousa.com/) |
||||
|
* [Using Microsoft AI Extensions Library and OpenAI to Summarize User Comments](https://abp.io/community/articles/using-microsoft-ai-extensions-library-and-openai-to-summarize-user-comments-gj1lusg7) by [Halil Ibrahim Kalkan](https://twitter.com/hibrahimkalkan) |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
## About the Next Version |
||||
|
|
||||
|
The next feature version will be 9.3. You can follow the [release planning here](https://github.com/abpframework/abp/milestones). Please [submit an issue](https://github.com/abpframework/abp/issues/new) if you have any problems with this version. |
||||
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 27 KiB 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 |
@ -0,0 +1,38 @@ |
|||||
|
You Asked, We Delivered: Introducing Our New Training Bootcamp\! |
||||
|
|
||||
|
Over the past months we’ve heard from countless freelancers, individuals and small teams asking for a focused, high-impact training without the overwhelming commitment or high costs of traditional bootcamps. So here we are\! |
||||
|
|
||||
|
We're excited to introduce **ABP Bootcamp: Mastering Infrastructure & Features**, a full training experience designed to help developers level up fast with practical knowledge they can apply immediately. |
||||
|
|
||||
|
This live, instructor-led program delivers four days of in-depth, real-world training on ABP’s infrastructure and core features. |
||||
|
|
||||
|
**Bootcamp Details** |
||||
|
|
||||
|
Dates: July 1,2,3,4 2025 |
||||
|
Time: 17:00-19:00 (UTC) each day |
||||
|
Format: Live, online sessions |
||||
|
Price: $399 (discounted $799) |
||||
|
Seats: Limited to a small group |
||||
|
|
||||
|
### **What You’ll Learn** |
||||
|
|
||||
|
By the end of this bootcamp, you’ll have: |
||||
|
|
||||
|
\-A solid understanding of ABP’s infrastructure, systems, and design patterns |
||||
|
\-Practical experience with features like dependency injection, configuration, logging, and localization |
||||
|
\-The ability to implement email templating, event bus, caching, and custom settings in real applications |
||||
|
\-Knowledge of best practices for building scalable, maintainable, and high-performance solutions with ABP |
||||
|
|
||||
|
### **Who It’s For** |
||||
|
|
||||
|
This bootcamp is ideal for: |
||||
|
|
||||
|
\-Developers and freelancers using ABP in their daily work |
||||
|
\-Teams looking to speed up onboarding and improve development efficiency |
||||
|
\-Anyone who wants practical ABP knowledge without committing to long-format training programs |
||||
|
|
||||
|
### **Reserve Your Spot** |
||||
|
|
||||
|
Seats are limited to keep the experience interactive and personalized. With the current 50% discount, this is the most accessible way to gain hands-on ABP experience with expert guidance. |
||||
|
|
||||
|
**Registration is now open. Secure your place today and master the infrastructure that powers ABP. Register here: [https://docs.google.com/forms/d/e/1FAIpQLSckMZChdqIJIuWiEvg\_KsSDmrxagRdc5WBYceAemHLrmT1oiA/viewform](https://docs.google.com/forms/d/e/1FAIpQLSckMZChdqIJIuWiEvg_KsSDmrxagRdc5WBYceAemHLrmT1oiA/viewform)** |
||||
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.5 MiB |
@ -0,0 +1,19 @@ |
|||||
|
@typeparam TEntity |
||||
|
@typeparam TResourceType |
||||
|
@using Volo.Abp.BlazoriseUI |
||||
|
@using Volo.Abp.Localization |
||||
|
@inherits ExtensionPropertyComponentBase<TEntity, TResourceType> |
||||
|
|
||||
|
@if (PropertyInfo != null && Entity != null) |
||||
|
{ |
||||
|
<Validation Validator="@Validate" MessageLocalizer="@LH.Localize"> |
||||
|
<Field> |
||||
|
<FieldLabel>@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)</FieldLabel> |
||||
|
<MemoEdit @bind-Text="@Value" AutoSize Disabled="IsReadonlyField"> |
||||
|
<Feedback> |
||||
|
<ValidationError/> |
||||
|
</Feedback> |
||||
|
</MemoEdit> |
||||
|
</Field> |
||||
|
</Validation> |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
using Volo.Abp.Data; |
||||
|
|
||||
|
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending; |
||||
|
public partial class TextAreaExtensionProperty<TEntity, TResourceType> |
||||
|
where TEntity : IHasExtraProperties |
||||
|
{ |
||||
|
protected string? Value { |
||||
|
get { |
||||
|
return PropertyInfo.GetTextInputValueOrNull(Entity.GetProperty(PropertyInfo.Name)); |
||||
|
} |
||||
|
set { |
||||
|
Entity.SetProperty(PropertyInfo.Name, value, validate: false); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus.RabbitMq; |
||||
|
|
||||
|
public class PostConfigureAbpRabbitMqEventBusOptions : IPostConfigureOptions<AbpRabbitMqEventBusOptions> |
||||
|
{ |
||||
|
private readonly HashSet<string> _uint64QueueArguments = |
||||
|
[ |
||||
|
"x-delivery-limit", |
||||
|
"x-expires", |
||||
|
"x-message-ttl", |
||||
|
"x-max-length", |
||||
|
"x-max-length-bytes", |
||||
|
"x-quorum-initial-group-size", |
||||
|
"x-quorum-target-group-size", |
||||
|
"x-stream-filter-size-bytes", |
||||
|
"x-stream-max-segment-size-bytes", |
||||
|
]; |
||||
|
|
||||
|
private readonly HashSet<string> _boolQueueArguments = |
||||
|
[ |
||||
|
"x-single-active-consumer" |
||||
|
]; |
||||
|
|
||||
|
public virtual void PostConfigure(string? name, AbpRabbitMqEventBusOptions options) |
||||
|
{ |
||||
|
ParseBoolQueueArguments(options); |
||||
|
ParseIntegerQueueArguments(options); |
||||
|
} |
||||
|
|
||||
|
protected virtual void ParseBoolQueueArguments(AbpRabbitMqEventBusOptions options) |
||||
|
{ |
||||
|
foreach (var argument in _boolQueueArguments) |
||||
|
{ |
||||
|
if (options.QueueArguments.TryGetValue(argument, out var value) && value is string stringValue && bool.TryParse(stringValue, out var boolValue)) |
||||
|
{ |
||||
|
options.QueueArguments[argument] = boolValue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual void ParseIntegerQueueArguments(AbpRabbitMqEventBusOptions options) |
||||
|
{ |
||||
|
foreach (var argument in _uint64QueueArguments) |
||||
|
{ |
||||
|
if (options.QueueArguments.TryGetValue(argument, out var value) && value is string stringValue && int.TryParse(stringValue, out var intValue)) |
||||
|
{ |
||||
|
options.QueueArguments[argument] = intValue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
using System.Collections.Concurrent; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using MongoDB.Driver; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.MongoDB.Clients; |
||||
|
|
||||
|
public class AbpMongoClientFactory : IAbpMongoClientFactory, ISingletonDependency |
||||
|
{ |
||||
|
protected ConcurrentDictionary<string, MongoClient> ClientCache { get; } |
||||
|
protected AbpMongoDbContextOptions Options { get; } |
||||
|
|
||||
|
public AbpMongoClientFactory(IOptions<AbpMongoDbContextOptions> options) |
||||
|
{ |
||||
|
Options = options.Value; |
||||
|
ClientCache = new ConcurrentDictionary<string, MongoClient>(); |
||||
|
} |
||||
|
|
||||
|
public virtual Task<MongoClient> GetAsync(MongoUrl mongoUrl) |
||||
|
{ |
||||
|
Check.NotNull(mongoUrl, nameof(mongoUrl)); |
||||
|
|
||||
|
return Task.FromResult( |
||||
|
ClientCache.GetOrAdd(mongoUrl.ToString(), _ => |
||||
|
{ |
||||
|
var mongoClientSettings = MongoClientSettings.FromUrl(mongoUrl); |
||||
|
Options.MongoClientSettingsConfigurer?.Invoke(mongoClientSettings); |
||||
|
return new MongoClient(mongoClientSettings); |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
public virtual MongoClient Get(MongoUrl mongoUrl) |
||||
|
{ |
||||
|
Check.NotNull(mongoUrl, nameof(mongoUrl)); |
||||
|
|
||||
|
return ClientCache.GetOrAdd(mongoUrl.ToString(), _ => |
||||
|
{ |
||||
|
var mongoClientSettings = MongoClientSettings.FromUrl(mongoUrl); |
||||
|
Options.MongoClientSettingsConfigurer?.Invoke(mongoClientSettings); |
||||
|
return new MongoClient(mongoClientSettings); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
|
||||
|
namespace Volo.Abp.MongoDB.Clients; |
||||
|
|
||||
|
public interface IAbpMongoClientFactory |
||||
|
{ |
||||
|
Task<MongoClient> GetAsync(MongoUrl mongoUrl); |
||||
|
|
||||
|
[Obsolete("Use GetAsync method")] |
||||
|
MongoClient Get(MongoUrl mongoUrl); |
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.MongoDB.Clients; |
||||
|
|
||||
|
[Collection(MongoTestCollection.Name)] |
||||
|
public class MongoClient_Factory_Tests : MongoDbTestBase |
||||
|
{ |
||||
|
private readonly IAbpMongoClientFactory _factory; |
||||
|
|
||||
|
public MongoClient_Factory_Tests() |
||||
|
{ |
||||
|
_factory = GetRequiredService<IAbpMongoClientFactory>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Return_Same_Instance_For_Same_ConnectionString() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var mongoUrl = new MongoUrl("mongodb://localhost:27017/my-db"); |
||||
|
|
||||
|
// Act
|
||||
|
var client1 = await _factory.GetAsync(mongoUrl); |
||||
|
var client2 = await _factory.GetAsync(mongoUrl); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.Same(client1, client2); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Return_Different_Instances_For_Different_ConnectionStrings() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var mongoUrl1 = new MongoUrl("mongodb://localhost:27017/db1"); |
||||
|
var mongoUrl2 = new MongoUrl("mongodb://localhost:27017/db2"); |
||||
|
|
||||
|
// Act
|
||||
|
var client1 = await _factory.GetAsync(mongoUrl1); |
||||
|
var client2 = await _factory.GetAsync(mongoUrl2); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.NotSame(client1, client2); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Not_Throw_For_Valid_But_Unreachable_Connection() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
var mongoUrl = new MongoUrl("mongodb://unreachablehost:12345/any"); |
||||
|
|
||||
|
// Act
|
||||
|
var client = await _factory.GetAsync(mongoUrl); |
||||
|
|
||||
|
// Assert
|
||||
|
Assert.NotNull(client); // Even though it's not connectable now, the instance can be created
|
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Be_ThreadSafe_When_Accessed_Concurrently() |
||||
|
{ |
||||
|
var mongoUrl = new MongoUrl("mongodb://localhost:27017/threadsafe"); |
||||
|
var results = new MongoClient[100]; |
||||
|
|
||||
|
await Parallel.ForAsync(0, 100, async (i, _) => |
||||
|
{ |
||||
|
results[i] = await _factory.GetAsync(mongoUrl); |
||||
|
}); |
||||
|
|
||||
|
Assert.All(results, client => Assert.Same(results[0], client)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Throw_If_ConnectionString_Is_Null_Or_Empty() |
||||
|
{ |
||||
|
await Assert.ThrowsAsync<ArgumentNullException>(() => _factory.GetAsync(null!)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
#ProfileManagementWrapper .tab-content { |
||||
|
padding-top: 0 !important; |
||||
|
} |
||||