mirror of https://github.com/abpframework/abp.git
374 changed files with 13049 additions and 708 deletions
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
@ -0,0 +1,475 @@ |
|||
# Building Dynamic XML Sitemaps with ABP Framework |
|||
|
|||
Search Engine Optimization (SEO) is crucial for any web application that wants to be discovered by users. One of the most fundamental SEO practices is providing a comprehensive XML sitemap that helps search engines crawl and index your website efficiently. In this article, we'll use a reusable ABP module that automatically generates dynamic XML sitemaps for both static Razor Pages and dynamic content from your database. |
|||
|
|||
By the end of this tutorial, you'll have a production-ready sitemap solution that discovers your pages automatically, includes dynamic content like blog posts or products, and regenerates sitemaps in the background without impacting performance. |
|||
|
|||
## What is an XML Sitemap? |
|||
|
|||
An XML sitemap is a file that lists all important pages of your website in a structured format that search engines can easily read. It acts as a roadmap for crawlers like Google, Bing, and others, telling them which pages exist, when they were last updated, and how they relate to each other. |
|||
|
|||
For modern web applications with dynamic content, manually maintaining sitemap files quickly becomes impractical. A dynamic sitemap solution that automatically discovers and updates URLs is essential for: |
|||
|
|||
- **Large content sites** with frequently changing blog posts, articles, or products |
|||
- **Multi-tenant applications** where each tenant may have different content |
|||
- **Enterprise applications** with complex page hierarchies |
|||
- **E-commerce platforms** with thousands of product pages |
|||
|
|||
## Why Build a Custom Sitemap Module? |
|||
|
|||
While there are general-purpose sitemap libraries available, building a custom module for ABP Framework provides several advantages: |
|||
|
|||
✅ **Deep ABP Integration**: Leverages ABP's dependency injection, background workers, and module system |
|||
✅ **Automatic Discovery**: Uses ASP.NET Core's Razor Page infrastructure to automatically find pages |
|||
✅ **Type-Safe Configuration**: Strongly-typed attributes and options for configuration |
|||
✅ **Multi-Group Support**: Organize sitemaps by logical groups (main, blog, products, etc.) |
|||
✅ **Background Generation**: Non-blocking sitemap regeneration using ABP's background worker system |
|||
✅ **Repository Integration**: Direct integration with ABP repositories for database entities |
|||
|
|||
## Project Architecture Overview |
|||
|
|||
Before using the module, let's understand its architecture: |
|||
|
|||
 |
|||
|
|||
The sitemap module consists of several key components: |
|||
|
|||
1. **Discovery Layer**: Discovers Razor Pages and their metadata using reflection |
|||
2. **Source Layer**: Defines contracts for providing sitemap items (static pages and dynamic content) |
|||
3. **Collection Layer**: Collects items from all registered sources |
|||
4. **Generation Layer**: Transforms collected items into XML format |
|||
5. **Management Layer**: Orchestrates file generation and background workers |
|||
|
|||
## Installation |
|||
|
|||
To get started, clone the demo repository which includes the sitemap module: |
|||
|
|||
```bash |
|||
git clone https://github.com/salihozkara/AbpSitemapDemo |
|||
cd AbpSitemapDemo |
|||
``` |
|||
|
|||
The repository contains the sitemap module in the `Modules/abp.sitemap/` directory. To use it in your own project, add a project reference: |
|||
|
|||
```xml |
|||
<ProjectReference Include="../Modules/abp.sitemap/Abp.Sitemap.Web/Abp.Sitemap.Web.csproj" /> |
|||
``` |
|||
|
|||
## Module Configuration |
|||
|
|||
After installing the package, add the module to your ABP application's module class: |
|||
|
|||
```csharp |
|||
using Abp.Sitemap.Web; |
|||
|
|||
[DependsOn( |
|||
typeof(SitemapWebModule), // 👈 Add sitemap module |
|||
// ... other dependencies |
|||
)] |
|||
public class YourProjectWebModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
// Configure sitemap options |
|||
Configure<SitemapOptions>(options => |
|||
{ |
|||
options.BaseUrl = "https://yourdomain.com"; // 👈 Your website URL |
|||
options.FolderPath = "Sitemaps"; // 👈 Where XML files are stored |
|||
options.WorkerPeriod = 3600000; // 👈 Regenerate every hour (in milliseconds) |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
> **Note:** In ABP applications, BaseUrl can be resolved from AppUrlOptions to stay consistent with environment configuration. |
|||
|
|||
That's it! The module is now integrated and will automatically: |
|||
- Discover your Razor Pages |
|||
- Generate sitemap XML files on application startup |
|||
- Regenerate sitemaps in the background every hour |
|||
|
|||
## Usage Examples |
|||
|
|||
Let's explore practical examples of using the sitemap module. You can see complete working examples in the [AbpSitemapDemo repository](https://github.com/salihozkara/AbpSitemapDemo). |
|||
|
|||
### Example 1: Mark Static Pages |
|||
|
|||
The simplest way to include pages in your sitemap is using attributes: |
|||
|
|||
```csharp |
|||
using Abp.Sitemap.Web.Sitemap.Sources.Page.Attributes; |
|||
|
|||
namespace YourProject.Pages; |
|||
|
|||
[IncludeSitemapXml] // 👈 Include in default "Main" group |
|||
public class IndexModel : PageModel |
|||
{ |
|||
public void OnGet() |
|||
{ |
|||
// Your page logic |
|||
} |
|||
} |
|||
|
|||
[IncludeSitemapXml(Group = "Help")] |
|||
public class FaqModel : PageModel |
|||
{ |
|||
public void OnGet() |
|||
{ |
|||
// Your page logic |
|||
} |
|||
} |
|||
``` |
|||
|
|||
These pages will be automatically discovered and included in the sitemap XML files. |
|||
|
|||
### Example 2: Add Dynamic Content from Database |
|||
|
|||
For dynamic content like blog posts, products, or articles, create a custom sitemap source. Here's a complete example using a Book entity: |
|||
|
|||
```csharp |
|||
using Abp.Sitemap.Web.Sitemap.Core; |
|||
using Abp.Sitemap.Web.Sitemap.Sources.Group; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace YourProject.Sitemaps; |
|||
|
|||
public class BookSitemapSource : GroupedSitemapItemSource<Book>, ITransientDependency |
|||
{ |
|||
public BookSitemapSource( |
|||
IReadOnlyRepository<Book> repository, |
|||
IAsyncQueryableExecuter executer) |
|||
: base(repository, executer, group: "Books") // 👈 Creates sitemap-Books.xml |
|||
{ |
|||
Filter = x => x.IsPublished; // 👈 Only published books |
|||
} |
|||
|
|||
protected override Expression<Func<Book, SitemapItem>> Selector => |
|||
book => new SitemapItem( |
|||
book.Id.ToString(), // 👈 Unique identifier |
|||
$"/Books/Detail/{book.Id}", // 👈 URL pattern matching your route |
|||
book.LastModificationTime ?? book.CreationTime // 👈 Last modified date |
|||
) |
|||
{ |
|||
ChangeFrequency = "weekly", |
|||
Priority = 0.7 |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
Key points: |
|||
- Inherits from `GroupedSitemapItemSource<TEntity>` |
|||
- Specifies the entity type (`Book`) |
|||
- Defines a group name ("Books") which creates `sitemap-Books.xml` |
|||
- Uses `Filter` to include only published books |
|||
- Maps entity properties to sitemap URLs using `Selector` |
|||
- Automatically registered via `ITransientDependency` |
|||
|
|||
### Example 3: Category-Based Dynamic Content |
|||
|
|||
For content with categories, you can build more complex URL patterns: |
|||
|
|||
```csharp |
|||
using Abp.Sitemap.Web.Sitemap.Core; |
|||
using Abp.Sitemap.Web.Sitemap.Sources.Group; |
|||
|
|||
namespace YourProject.Sitemaps; |
|||
|
|||
public class ArticleSitemapSource : GroupedSitemapItemSource<Article>, ITransientDependency |
|||
{ |
|||
public ArticleSitemapSource( |
|||
IReadOnlyRepository<Article> repository, |
|||
IAsyncQueryableExecuter executer) |
|||
: base(repository, executer, "Articles") |
|||
{ |
|||
// Multiple filter conditions |
|||
Filter = x => x.IsPublished && |
|||
!x.IsDeleted && |
|||
x.PublishDate <= DateTime.Now; |
|||
} |
|||
|
|||
protected override Expression<Func<Article, SitemapItem>> Selector => |
|||
article => new SitemapItem( |
|||
article.Id.ToString(), |
|||
$"/blog/{article.Category.Slug}/{article.Slug}", // 👈 Category-based URL |
|||
article.LastModificationTime ?? article.CreationTime |
|||
); |
|||
} |
|||
``` |
|||
|
|||
This example demonstrates: |
|||
- Multiple filter conditions for complex business logic |
|||
- Building URLs with category slugs |
|||
|
|||
## Testing Your Sitemaps |
|||
|
|||
After configuring the module, test your sitemap generation: |
|||
|
|||
### 1. Run Your Application |
|||
|
|||
```bash |
|||
dotnet run |
|||
``` |
|||
|
|||
The sitemaps are automatically generated on application startup. |
|||
|
|||
### 2. Check Generated Files |
|||
|
|||
Navigate to `{WebProject}/Sitemaps/` directory (at the root of your web project): |
|||
|
|||
``` |
|||
{WebProject} |
|||
└── Sitemaps/ |
|||
├── sitemap.xml # Main group (static pages) |
|||
├── sitemap-Books.xml # Books from database |
|||
├── sitemap-Articles.xml # Articles from database |
|||
└── sitemap-Help.xml # Help pages |
|||
``` |
|||
|
|||
### 3. Verify XML Content |
|||
|
|||
Open `sitemap-Books.xml` and verify the structure: |
|||
|
|||
```xml |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> |
|||
<url> |
|||
<loc>https://yourdomain.com/Books/Detail/3a071e39-12c9-48d7-8c1e-3b4f5c6d7e8f</loc> |
|||
<lastmod>2025-12-13</lastmod> |
|||
</url> |
|||
<url> |
|||
<loc>https://yourdomain.com/Books/Detail/7b8c9d0e-1f2a-3b4c-5d6e-7f8g9h0i1j2k</loc> |
|||
<lastmod>2025-12-10</lastmod> |
|||
</url> |
|||
</urlset> |
|||
``` |
|||
|
|||
### 4. Test in Browser |
|||
|
|||
Visit the sitemap URLs directly (the module serves them from the root path): |
|||
- Main sitemap: `https://localhost:5001/sitemap.xml` |
|||
- Books sitemap: `https://localhost:5001/sitemap-Books.xml` |
|||
|
|||
> **Note:** The sitemaps are stored in `{WebProject}/Sitemaps/` directory and served directly from the root URL. |
|||
|
|||
## Advanced Configuration |
|||
|
|||
### Custom Regeneration Schedule |
|||
|
|||
Control when sitemaps are regenerated using cron expressions: |
|||
|
|||
```csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<SitemapOptions>(options => |
|||
{ |
|||
options.BaseUrl = "https://yourdomain.com"; |
|||
options.WorkerCronExpression = "0 0 2 * * ?"; // 👈 Every day at 2 AM |
|||
// Or use period in milliseconds: |
|||
// options.WorkerPeriod = 7200000; // 2 hours |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
### Environment-Specific Configuration |
|||
|
|||
Use different settings for development and production: |
|||
|
|||
```csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var configuration = context.Services.GetConfiguration(); |
|||
var hostingEnvironment = context.Services.GetHostingEnvironment(); |
|||
|
|||
Configure<SitemapOptions>(options => |
|||
{ |
|||
if (hostingEnvironment.IsDevelopment()) |
|||
{ |
|||
options.BaseUrl = "https://localhost:5001"; |
|||
options.WorkerPeriod = 300000; // 5 minutes for testing |
|||
} |
|||
else |
|||
{ |
|||
options.BaseUrl = configuration["App:SelfUrl"]!; |
|||
options.WorkerPeriod = 3600000; // 1 hour in production |
|||
} |
|||
|
|||
options.FolderPath = "Sitemaps"; |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
### Manual Sitemap Generation |
|||
|
|||
Trigger sitemap generation manually (useful for admin panels): |
|||
|
|||
```csharp |
|||
using Abp.Sitemap.Web.Sitemap.Management; |
|||
|
|||
public class SitemapManagementService : ITransientDependency |
|||
{ |
|||
private readonly SitemapFileGenerator _generator; |
|||
|
|||
public SitemapManagementService(SitemapFileGenerator generator) |
|||
{ |
|||
_generator = generator; |
|||
} |
|||
|
|||
[Authorize("Admin")] |
|||
public async Task RegenerateSitemapsAsync() |
|||
{ |
|||
await _generator.GenerateAsync(); // 👈 Manual regeneration |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Real-World Use Cases |
|||
|
|||
Here are practical scenarios where the sitemap module excels: |
|||
|
|||
### E-Commerce Platform |
|||
```csharp |
|||
// Products grouped by category |
|||
public class ProductSitemapSource : GroupedSitemapItemSource<Product> |
|||
{ |
|||
// Automatically includes all active products with stock |
|||
} |
|||
|
|||
// Separate sitemap for categories |
|||
public class CategorySitemapSource : GroupedSitemapItemSource<Category> |
|||
{ |
|||
// All browsable categories |
|||
} |
|||
|
|||
// Brand pages |
|||
public class BrandSitemapSource : GroupedSitemapItemSource<Brand> |
|||
{ |
|||
// All active brands |
|||
} |
|||
``` |
|||
|
|||
Result: `sitemap-Products.xml`, `sitemap-Categories.xml`, `sitemap-Brands.xml` |
|||
|
|||
### Content Management System |
|||
```csharp |
|||
// Blog posts by date |
|||
public class BlogPostSitemapSource : GroupedSitemapItemSource<BlogPost> |
|||
{ |
|||
// Filter by published date, priority based on view count |
|||
} |
|||
|
|||
// Static CMS pages |
|||
[IncludeSitemapXml] |
|||
public class AboutUsModel : PageModel { } |
|||
``` |
|||
|
|||
## Best Practices |
|||
|
|||
### 1. Group Related Content |
|||
Organize your sitemaps logically: |
|||
```csharp |
|||
// ✅ Good: Logical grouping |
|||
"Products", "Categories", "Brands", "Blog", "Help" |
|||
|
|||
// ❌ Bad: Everything in one group |
|||
"Main" // Contains 50,000 mixed URLs |
|||
``` |
|||
|
|||
### 2. Use Filters Wisely |
|||
```csharp |
|||
// ✅ Good: Only published, non-deleted content |
|||
Filter = x => x.IsPublished && |
|||
!x.IsDeleted && |
|||
x.PublishDate <= DateTime.Now |
|||
|
|||
// ❌ Bad: Including draft content |
|||
Filter = x => true // Everything included |
|||
``` |
|||
|
|||
### 3. Keep URLs Clean |
|||
```csharp |
|||
// ✅ Good: SEO-friendly URLs |
|||
$"/products/{product.Slug}" |
|||
$"/blog/{year}/{month}/{article.Slug}" |
|||
|
|||
// ❌ Bad: Technical IDs exposed |
|||
$"/product-detail?id={product.Id}" |
|||
``` |
|||
|
|||
## Troubleshooting |
|||
|
|||
### Sitemap Not Generated |
|||
**Problem:** No XML files in `{WebProject}/Sitemaps/` |
|||
|
|||
**Solutions:** |
|||
1. Check module is added to dependencies |
|||
2. Verify `SitemapOptions.BaseUrl` is configured |
|||
3. Check application logs for errors |
|||
4. Ensure the web project directory has write permissions |
|||
|
|||
### Pages Not Appearing |
|||
**Problem:** Some pages missing from sitemap |
|||
|
|||
**Solutions:** |
|||
1. Verify `[IncludeSitemapXml]` attribute is present |
|||
2. Check namespace imports: `using Abp.Sitemap.Web.Sitemap.Sources.Page.Attributes;` |
|||
3. Ensure PageModel classes are public |
|||
4. Check filter conditions in custom sources |
|||
|
|||
### Background Worker Not Running |
|||
**Problem:** Sitemaps not regenerating automatically |
|||
|
|||
**Solutions:** |
|||
1. Check `SitemapOptions.WorkerPeriod` is set |
|||
2. Verify background workers are enabled in ABP configuration |
|||
3. Check application logs for worker errors |
|||
|
|||
## Performance Considerations |
|||
|
|||
### Caching Strategy |
|||
Consider adding caching for frequently accessed sitemaps: |
|||
|
|||
```csharp |
|||
public class CachedSitemapFileGenerator : ITransientDependency |
|||
{ |
|||
private readonly SitemapFileGenerator _generator; |
|||
private readonly IDistributedCache _cache; |
|||
|
|||
public async Task<string> GetOrGenerateAsync(string group) |
|||
{ |
|||
var cacheKey = $"Sitemap:{group}"; |
|||
var cached = await _cache.GetStringAsync(cacheKey); |
|||
|
|||
if (cached != null) |
|||
return cached; |
|||
|
|||
await _generator.GenerateAsync(); |
|||
// Read and cache... |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Conclusion |
|||
|
|||
The ABP Sitemap module provides a production-ready solution for dynamic sitemap generation in ABP Framework applications. By leveraging ABP's architecture—dependency injection, repository pattern, and background workers—the module automatically discovers pages, includes dynamic content, and regenerates sitemaps without manual intervention. |
|||
|
|||
Key benefits: |
|||
✅ **Zero Configuration** for basic scenarios |
|||
✅ **Type-Safe** attribute-based configuration |
|||
✅ **Extensible** for complex business logic |
|||
✅ **Performance** optimized with background processing |
|||
✅ **SEO-Friendly** following XML sitemap standards |
|||
|
|||
Whether you're building a blog, e-commerce platform, or enterprise application, this module provides a solid foundation for search engine optimization. |
|||
|
|||
## Additional Resources |
|||
|
|||
### Documentation |
|||
- [ABP Framework Documentation](https://abp.io/docs/latest/) |
|||
- [ABP Background Workers](https://abp.io/docs/latest/framework/infrastructure/background-workers) |
|||
- [ABP Repository Pattern](https://abp.io/docs/latest/framework/architecture/domain-driven-design/repositories) |
|||
- [ABP Dependency Injection](https://abp.io/docs/latest/framework/fundamentals/dependency-injection) |
|||
|
|||
### Source Code |
|||
- [Complete Working Demo](https://github.com/salihozkara/AbpSitemapDemo) - Full implementation with examples |
|||
- [BookSitemapSource](https://github.com/salihozkara/AbpSitemapDemo/blob/master/AbpSitemapDemo/Pages/Books/Index.cshtml.cs#L23) - Entity-based source example |
|||
- [Index.cshtml](https://github.com/salihozkara/AbpSitemapDemo/blob/master/AbpSitemapDemo/Pages/Index.cshtml#L9) - Page attribute usage |
|||
@ -0,0 +1 @@ |
|||
Learn how to use the ABP Sitemap module for automatic XML sitemap generation in your ABP Framework applications. |
|||
@ -0,0 +1,218 @@ |
|||
# Microsoft.Agents.AI (Agent Framework) |
|||
|
|||
[Microsoft Agent Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview) is an open-source development kit for **building AI agents** and **multi-agent workflows**. It is the direct successor to both *AutoGen* and [*Semantic Kernel*](./microsoft-semantic-kernel.md), combining their strengths while adding new capabilities, and is the suggested framework for building AI agent applications. This documentation is about the usage of this library with ABP Framework. Make sure you have read the [Artificial Intelligence](./index.md) documentation before reading this documentation. |
|||
|
|||
## Usage |
|||
|
|||
**Microsoft Agent Framework** works on top of `IChatClient` from **Microsoft.Extensions.AI**. After obtaining an `IChatClient` instance, you can create an AI agent using the `CreateAIAgent` extension method: |
|||
|
|||
```csharp |
|||
using Microsoft.Agents.AI; |
|||
using Microsoft.Extensions.AI; |
|||
|
|||
public class MyService |
|||
{ |
|||
private readonly IChatClient _chatClient; |
|||
|
|||
public MyService(IChatClient chatClient) |
|||
{ |
|||
_chatClient = chatClient; |
|||
} |
|||
|
|||
public async Task<string> GetResponseAsync(string userMessage) |
|||
{ |
|||
AIAgent agent = _chatClient.CreateAIAgent( |
|||
instructions: "You are a helpful assistant that provides concise answers." |
|||
); |
|||
|
|||
AgentRunResponse response = await agent.RunAsync(userMessage); |
|||
|
|||
return response.Text; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
You can also use `IChatClientAccessor` to access the `IChatClient` in scenarios where AI capabilities are **optional**, such as when developing a module or a service that may use AI capabilities optionally: |
|||
|
|||
```csharp |
|||
using Microsoft.Agents.AI; |
|||
using Microsoft.Extensions.AI; |
|||
using Volo.Abp.AI; |
|||
|
|||
public class MyService |
|||
{ |
|||
private readonly IChatClientAccessor _chatClientAccessor; |
|||
|
|||
public MyService(IChatClientAccessor chatClientAccessor) |
|||
{ |
|||
_chatClientAccessor = chatClientAccessor; |
|||
} |
|||
|
|||
public async Task<string> GetResponseAsync(string userMessage) |
|||
{ |
|||
var chatClient = _chatClientAccessor.ChatClient; |
|||
if (chatClient is null) |
|||
{ |
|||
return "No chat client configured"; |
|||
} |
|||
|
|||
AIAgent agent = chatClient.CreateAIAgent( |
|||
instructions: "You are a helpful assistant that provides concise answers." |
|||
); |
|||
|
|||
var response = await agent.RunAsync(userMessage); |
|||
|
|||
return response.Text; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### Workspaces |
|||
|
|||
Workspaces are a way to configure isolated AI configurations for a named scope. You can define a workspace by decorating a class with the `WorkspaceNameAttribute` attribute that carries the workspace name. |
|||
- Workspace names must be unique. |
|||
- Workspace names cannot contain spaces _(use underscores or camelCase)_. |
|||
- Workspace names are case-sensitive. |
|||
|
|||
```csharp |
|||
using Volo.Abp.AI; |
|||
|
|||
[WorkspaceName("CommentSummarization")] |
|||
public class CommentSummarization |
|||
{ |
|||
} |
|||
``` |
|||
|
|||
> [!NOTE] |
|||
> If you don't specify the workspace name, the full name of the class will be used as the workspace name. |
|||
|
|||
You can resolve generic versions of `IChatClient` and `IChatClientAccessor` services for a specific workspace as generic arguments. If Chat Client is not configured for a workspace, you will get `null` from the accessor services. You should check the accessor before using it. This applies only for specified workspaces. Another workspace may have a configured Chat Client. |
|||
|
|||
`IChatClient<TWorkSpace>` or `IChatClientAccessor<TWorkSpace>` can be resolved to access a specific workspace's chat client. This is a typed chat client and can be configured separately from the default chat client. |
|||
|
|||
Example of resolving a typed chat client for a workspace: |
|||
|
|||
```csharp |
|||
using Microsoft.Agents.AI; |
|||
using Microsoft.Extensions.AI; |
|||
using Volo.Abp.AI; |
|||
|
|||
public class MyService |
|||
{ |
|||
private readonly IChatClient<CustomerSupport> _chatClient; |
|||
|
|||
public MyService(IChatClient<CustomerSupport> chatClient) |
|||
{ |
|||
_chatClient = chatClient; |
|||
} |
|||
|
|||
public async Task<string> GetResponseAsync(string userMessage) |
|||
{ |
|||
AIAgent agent = _chatClient.CreateAIAgent( |
|||
instructions: "You are a customer support assistant. Be polite and helpful." |
|||
); |
|||
|
|||
var response = await agent.RunAsync(userMessage); |
|||
return response.Text; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Example of resolving a typed chat client accessor for a workspace: |
|||
|
|||
```csharp |
|||
using Microsoft.Agents.AI; |
|||
using Microsoft.Extensions.AI; |
|||
using Volo.Abp.AI; |
|||
|
|||
public class MyService |
|||
{ |
|||
private readonly IChatClientAccessor<CustomerSupport> _chatClientAccessor; |
|||
|
|||
public MyService(IChatClientAccessor<CustomerSupport> chatClientAccessor) |
|||
{ |
|||
_chatClientAccessor = chatClientAccessor; |
|||
} |
|||
|
|||
public async Task<string> GetResponseAsync(string userMessage) |
|||
{ |
|||
var chatClient = _chatClientAccessor.ChatClient; |
|||
if (chatClient is null) |
|||
{ |
|||
return "No chat client configured"; |
|||
} |
|||
|
|||
AIAgent agent = chatClient.CreateAIAgent( |
|||
instructions: "You are a customer support assistant. Be polite and helpful." |
|||
); |
|||
|
|||
var response = await agent.RunAsync(userMessage); |
|||
return response.Text; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Configuration |
|||
|
|||
**Microsoft Agent Framework** uses `IChatClient` from **Microsoft.Extensions.AI** as its foundation. Therefore, the configuration process for workspaces is the same as described in the [Microsoft.Extensions.AI documentation](./microsoft-extensions-ai.md#configuration). |
|||
|
|||
You need to configure the Chat Client for your workspace using `AbpAIWorkspaceOptions`, and then you can use the `CreateAIAgent` extension method to create AI agents from the configured chat client. |
|||
|
|||
To configure a chat client, you'll need a LLM provider package such as [Microsoft.Extensions.AI.OpenAI](https://www.nuget.org/packages/Microsoft.Extensions.AI.OpenAI) or [OllamaSharp](https://www.nuget.org/packages/OllamaSharp/). |
|||
|
|||
_The following example requires [OllamaSharp](https://www.nuget.org/packages/OllamaSharp/) package to be installed._ |
|||
|
|||
Demonstration of the default workspace configuration: |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpAIModule))] |
|||
public class MyProjectModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
PreConfigure<AbpAIWorkspaceOptions>(options => |
|||
{ |
|||
options.Workspaces.ConfigureDefault(configuration => |
|||
{ |
|||
configuration.ConfigureChatClient(chatClientConfiguration => |
|||
{ |
|||
chatClientConfiguration.Builder = new ChatClientBuilder( |
|||
sp => new OllamaApiClient("http://localhost:11434", "mistral") |
|||
); |
|||
}); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Demonstration of the isolated workspace configuration: |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpAIModule))] |
|||
public class MyProjectModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
PreConfigure<AbpAIWorkspaceOptions>(options => |
|||
{ |
|||
options.Workspaces.Configure<CustomerSupport>(configuration => |
|||
{ |
|||
configuration.ConfigureChatClient(chatClientConfiguration => |
|||
{ |
|||
chatClientConfiguration.Builder = new ChatClientBuilder( |
|||
sp => new OllamaApiClient("http://localhost:11434", "mistral") |
|||
); |
|||
}); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## See Also |
|||
|
|||
- [Usage of Microsoft.Extensions.AI](./microsoft-extensions-ai.md) |
|||
- [Usage of Semantic Kernel](./microsoft-semantic-kernel.md) |
|||
- [Microsoft Agent Framework Overview](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview) |
|||
- [AI Samples for .NET](https://learn.microsoft.com/en-us/samples/dotnet/ai-samples/ai-samples/) |
|||
|
After Width: | Height: | Size: 3.7 KiB |
@ -0,0 +1,8 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public interface IHasResourcePermissions : IKeyedObject |
|||
{ |
|||
Dictionary<string, bool> ResourcePermissions { get; } |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public interface IResourcePermissionChecker |
|||
{ |
|||
Task<bool> IsGrantedAsync( |
|||
string name, |
|||
string resourceName, |
|||
string resourceKey |
|||
); |
|||
|
|||
Task<bool> IsGrantedAsync( |
|||
ClaimsPrincipal? claimsPrincipal, |
|||
string name, |
|||
string resourceName, |
|||
string resourceKey |
|||
); |
|||
|
|||
Task<MultiplePermissionGrantResult> IsGrantedAsync( |
|||
string[] names, |
|||
string resourceName, |
|||
string resourceKey |
|||
); |
|||
|
|||
Task<MultiplePermissionGrantResult> IsGrantedAsync( |
|||
ClaimsPrincipal? claimsPrincipal, |
|||
string[] names, |
|||
string resourceName, |
|||
string resourceKey |
|||
); |
|||
} |
|||
@ -0,0 +1,83 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public interface IResourcePermissionStore |
|||
{ |
|||
/// <summary>
|
|||
/// Checks if the given permission is granted for the given resource.
|
|||
/// </summary>
|
|||
/// <param name="name">The name of the permission.</param>
|
|||
/// <param name="resourceName">The name of the resource.</param>
|
|||
/// <param name="resourceKey">Resource key</param>
|
|||
/// <param name="providerName">The name of the provider.</param>
|
|||
/// <param name="providerKey">The key of the provider.</param>
|
|||
/// <returns>
|
|||
/// True if the permission is granted.
|
|||
/// </returns>
|
|||
Task<bool> IsGrantedAsync( |
|||
string name, |
|||
string resourceName, |
|||
string resourceKey, |
|||
string providerName, |
|||
string providerKey |
|||
); |
|||
|
|||
/// <summary>
|
|||
/// Checks if the given permissions are granted for the given resource.
|
|||
/// </summary>
|
|||
/// <param name="names">The name of the permissions.</param>
|
|||
/// <param name="resourceName">The name of the resource.</param>
|
|||
/// <param name="resourceKey">Resource key</param>
|
|||
/// <param name="providerName">The name of the provider.</param>
|
|||
/// <param name="providerKey">The key of the provider.</param>
|
|||
/// <returns>
|
|||
/// A <see cref="MultiplePermissionGrantResult"/> object containing the grant results for each permission.
|
|||
/// </returns>
|
|||
Task<MultiplePermissionGrantResult> IsGrantedAsync( |
|||
string[] names, |
|||
string resourceName, |
|||
string resourceKey, |
|||
string providerName, |
|||
string providerKey |
|||
); |
|||
|
|||
/// <summary>
|
|||
/// Gets all permissions for the given resource.
|
|||
/// </summary>
|
|||
/// <param name="resourceName">Resource name</param>
|
|||
/// <param name="resourceKey">Resource key</param>
|
|||
/// <returns>
|
|||
/// A <see cref="MultiplePermissionGrantResult"/> object containing the grant results for each permission.
|
|||
/// </returns>
|
|||
Task<MultiplePermissionGrantResult> GetPermissionsAsync( |
|||
string resourceName, |
|||
string resourceKey |
|||
); |
|||
|
|||
/// <summary>
|
|||
/// Gets all granted permissions for the given resource.
|
|||
/// </summary>
|
|||
/// <param name="resourceName">Resource name</param>
|
|||
/// <param name="resourceKey">Resource key</param>
|
|||
/// <returns>
|
|||
/// An array of granted permission names.
|
|||
/// </returns>
|
|||
Task<string[]> GetGrantedPermissionsAsync( |
|||
string resourceName, |
|||
string resourceKey |
|||
); |
|||
|
|||
/// <summary>
|
|||
/// Retrieves the keys of resources for which the specified permission is granted.
|
|||
/// </summary>
|
|||
/// <param name="resourceName">The name of the resource.</param>
|
|||
/// <param name="name">The name of the permission.</param>
|
|||
/// <returns>
|
|||
/// An array of resource keys where the specified permission is granted.
|
|||
/// </returns>
|
|||
Task<string[]> GetGrantedResourceKeysAsync( |
|||
string resourceName, |
|||
string name |
|||
); |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public interface IResourcePermissionValueProvider |
|||
{ |
|||
string Name { get; } |
|||
|
|||
Task<PermissionGrantResult> CheckAsync(ResourcePermissionValueCheckContext context); |
|||
|
|||
Task<MultiplePermissionGrantResult> CheckAsync(ResourcePermissionValuesCheckContext context); |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public interface IResourcePermissionValueProviderManager |
|||
{ |
|||
IReadOnlyList<IResourcePermissionValueProvider> ValueProviders { get; } |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public class NullResourcePermissionStore : IResourcePermissionStore, ISingletonDependency |
|||
{ |
|||
public ILogger<NullResourcePermissionStore> Logger { get; set; } |
|||
|
|||
public NullResourcePermissionStore() |
|||
{ |
|||
Logger = NullLogger<NullResourcePermissionStore>.Instance; |
|||
} |
|||
|
|||
public Task<bool> IsGrantedAsync(string name, string resourceName, string resourceKey, string providerName, string providerKey) |
|||
{ |
|||
return TaskCache.FalseResult; |
|||
} |
|||
|
|||
public Task<MultiplePermissionGrantResult> IsGrantedAsync(string[] names, string resourceName, string resourceKey, string providerName, string providerKey) |
|||
{ |
|||
return Task.FromResult(new MultiplePermissionGrantResult(names, PermissionGrantResult.Prohibited)); |
|||
} |
|||
|
|||
public Task<MultiplePermissionGrantResult> GetPermissionsAsync(string resourceName, string resourceKey) |
|||
{ |
|||
return Task.FromResult(new MultiplePermissionGrantResult()); |
|||
} |
|||
|
|||
public Task<string[]> GetGrantedPermissionsAsync(string resourceName, string resourceKey) |
|||
{ |
|||
return Task.FromResult(Array.Empty<string>()); |
|||
} |
|||
|
|||
public Task<string[]> GetGrantedResourceKeysAsync(string resourceName, string name) |
|||
{ |
|||
return Task.FromResult(Array.Empty<string>()); |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public static class ResourcePermissionCheckerExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Checks if a specific permission is granted for a resource with a given key.
|
|||
/// </summary>
|
|||
/// <typeparam name="TResource">The type of the resource.</typeparam>
|
|||
/// <param name="resourcePermissionChecker">The resource permission checker instance.</param>
|
|||
/// <param name="permissionName">The name of the permission to check.</param>
|
|||
/// <param name="resource">The resource instance to check permission for.</param>
|
|||
/// <param name="resourceKey">The unique key identifying the resource instance.</param>
|
|||
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean value indicating whether the permission is granted.</returns>
|
|||
public static Task<bool> IsGrantedAsync<TResource>( |
|||
this IResourcePermissionChecker resourcePermissionChecker, |
|||
string permissionName, |
|||
TResource resource, |
|||
object resourceKey |
|||
) |
|||
{ |
|||
Check.NotNull(resourcePermissionChecker, nameof(resourcePermissionChecker)); |
|||
Check.NotNullOrWhiteSpace(permissionName, nameof(permissionName)); |
|||
Check.NotNull(resource, nameof(resource)); |
|||
Check.NotNull(resourceKey, nameof(resourceKey)); |
|||
|
|||
return resourcePermissionChecker.IsGrantedAsync( |
|||
permissionName, |
|||
typeof(TResource).FullName!, |
|||
resourceKey.ToString()! |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public class ResourcePermissionGrantInfo : PermissionGrantInfo |
|||
{ |
|||
public string ResourceName { get; } |
|||
|
|||
public string ResourceKey { get; } |
|||
|
|||
public ResourcePermissionGrantInfo( |
|||
string name, |
|||
bool isGranted, |
|||
string resourceName, |
|||
string resourceKey, |
|||
string? providerName = null, |
|||
string? providerKey = null) |
|||
: base(name, isGranted, providerName, providerKey) |
|||
{ |
|||
ResourceName = resourceName; |
|||
ResourceKey = resourceKey; |
|||
} |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public static class ResourcePermissionStoreExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Retrieves the list of granted permissions for a specific resource with a given key.
|
|||
/// </summary>
|
|||
/// <typeparam name="TResource">The type of the resource.</typeparam>
|
|||
/// <param name="resourcePermissionStore">The resource permission store instance.</param>
|
|||
/// <param name="resource">The resource instance to retrieve permissions for.</param>
|
|||
/// <param name="resourceKey">The unique key identifying the resource instance.</param>
|
|||
/// <returns>A task that represents the asynchronous operation. The task result contains an array of strings representing the granted permissions.</returns>
|
|||
public static async Task<string[]> GetGrantedPermissionsAsync<TResource>( |
|||
this IResourcePermissionStore resourcePermissionStore, |
|||
TResource resource, |
|||
object resourceKey |
|||
) |
|||
{ |
|||
return (await GetPermissionsAsync(resourcePermissionStore, resource, resourceKey)).Where(x => x.Value).Select(x => x.Key).ToArray(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Retrieves a dictionary of permissions and their granted status for the specified entity.
|
|||
/// </summary>
|
|||
/// <typeparam name="TResource">The type of the resource.</typeparam>
|
|||
/// <param name="resourcePermissionStore">The resource permission store instance.</param>
|
|||
/// <param name="resource">The resource for which the permissions are being retrieved.</param>
|
|||
/// <param name="resourceKey">The unique key identifying the resource instance.</param>
|
|||
/// <returns>A dictionary where the keys are permission names and the values are booleans indicating whether the permission is granted.</returns>
|
|||
public static async Task<IDictionary<string, bool>> GetPermissionsAsync<TResource>( |
|||
this IResourcePermissionStore resourcePermissionStore, |
|||
TResource resource, |
|||
object resourceKey |
|||
) |
|||
{ |
|||
Check.NotNull(resourcePermissionStore, nameof(resourcePermissionStore)); |
|||
Check.NotNull(resource, nameof(resource)); |
|||
Check.NotNull(resourceKey, nameof(resourceKey)); |
|||
|
|||
var result = await resourcePermissionStore.GetPermissionsAsync( |
|||
typeof(TResource).FullName!, |
|||
resourceKey.ToString()! |
|||
); |
|||
|
|||
return result.Result.ToDictionary(x => x.Key, x => x.Value == PermissionGrantResult.Granted); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Retrieves the keys of the resources granted a specific permission.
|
|||
/// </summary>
|
|||
/// <typeparam name="TResource">The type of the resource.</typeparam>
|
|||
/// <param name="resourcePermissionStore">The resource permission store instance.</param>
|
|||
/// <param name="resource">The resource instance to check granted permissions for.</param>
|
|||
/// <param name="permissionName">The name of the permission to check.</param>
|
|||
/// <returns>A task that represents the asynchronous operation. The task result contains an array of strings representing the granted resource keys.</returns>
|
|||
public static Task<string[]> GetGrantedResourceKeysAsync<TResource>( |
|||
this IResourcePermissionStore resourcePermissionStore, |
|||
TResource resource, |
|||
string permissionName |
|||
) |
|||
{ |
|||
Check.NotNull(resourcePermissionStore, nameof(resourcePermissionStore)); |
|||
Check.NotNull(resource, nameof(resource)); |
|||
Check.NotNullOrWhiteSpace(permissionName, nameof(permissionName)); |
|||
|
|||
return resourcePermissionStore.GetGrantedResourceKeysAsync( |
|||
typeof(TResource).FullName!, |
|||
permissionName |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System.Security.Claims; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public class ResourcePermissionValueCheckContext : PermissionValueCheckContext |
|||
{ |
|||
public string ResourceName { get; } |
|||
|
|||
public string ResourceKey { get; } |
|||
|
|||
public ResourcePermissionValueCheckContext(PermissionDefinition permission, string resourceName, string resourceKey) |
|||
: this(permission, null, resourceName, resourceKey) |
|||
{ |
|||
} |
|||
|
|||
public ResourcePermissionValueCheckContext(PermissionDefinition permission, ClaimsPrincipal? principal, string resourceName, string resourceKey) |
|||
: base(permission, principal) |
|||
{ |
|||
ResourceName = resourceName; |
|||
ResourceKey = resourceKey; |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public abstract class ResourcePermissionValueProvider : IResourcePermissionValueProvider, ITransientDependency |
|||
{ |
|||
public abstract string Name { get; } |
|||
|
|||
protected IResourcePermissionStore ResourcePermissionStore { get; } |
|||
|
|||
protected ResourcePermissionValueProvider(IResourcePermissionStore resourcePermissionStore) |
|||
{ |
|||
ResourcePermissionStore = resourcePermissionStore; |
|||
} |
|||
|
|||
public abstract Task<PermissionGrantResult> CheckAsync(ResourcePermissionValueCheckContext context); |
|||
|
|||
public abstract Task<MultiplePermissionGrantResult> CheckAsync(ResourcePermissionValuesCheckContext context); |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
using System.Collections.Generic; |
|||
using System.Security.Claims; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public class ResourcePermissionValuesCheckContext : PermissionValuesCheckContext |
|||
{ |
|||
public string ResourceName { get; } |
|||
|
|||
public string ResourceKey { get; } |
|||
|
|||
public ResourcePermissionValuesCheckContext(PermissionDefinition permission,string resourceName, string resourceKey) |
|||
: this([permission], null, resourceName, resourceKey) |
|||
{ |
|||
|
|||
} |
|||
|
|||
|
|||
public ResourcePermissionValuesCheckContext(PermissionDefinition permission, ClaimsPrincipal? principal, string resourceName, string resourceKey) |
|||
: this([permission], principal, resourceName, resourceKey) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public ResourcePermissionValuesCheckContext(List<PermissionDefinition> permissions, string resourceName, string resourceKey) |
|||
: this(permissions, null, resourceName, resourceKey) |
|||
{ |
|||
ResourceName = resourceName; |
|||
ResourceKey = resourceKey; |
|||
} |
|||
|
|||
public ResourcePermissionValuesCheckContext(List<PermissionDefinition> permissions, ClaimsPrincipal? principal, string resourceName, string resourceKey) |
|||
: base(permissions, principal) |
|||
{ |
|||
ResourceName = resourceName; |
|||
ResourceKey = resourceKey; |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using JetBrains.Annotations; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
|
|||
namespace Volo.Abp.Authorization; |
|||
|
|||
public class ResourcePermissionRequirement : IAuthorizationRequirement |
|||
{ |
|||
public string PermissionName { get; } |
|||
|
|||
public ResourcePermissionRequirement([NotNull] string permissionName) |
|||
{ |
|||
Check.NotNull(permissionName, nameof(permissionName)); |
|||
|
|||
PermissionName = permissionName; |
|||
} |
|||
|
|||
public override string ToString() |
|||
{ |
|||
return $"ResourcePermissionRequirement: {PermissionName}"; |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection; |
|||
|
|||
public static class KeyedObjectResourcePermissionExtenstions |
|||
{ |
|||
public static IServiceCollection AddKeyedObjectResourcePermissionAuthorization(this IServiceCollection services) |
|||
{ |
|||
services.AddSingleton<IAuthorizationHandler, KeyedObjectResourcePermissionRequirementHandler>(); |
|||
return services; |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public static class KeyedObjectResourcePermissionCheckerExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Checks if the specified permission is granted for the given resource.
|
|||
/// </summary>
|
|||
/// <typeparam name="TResource">The type of the object.</typeparam>
|
|||
/// <param name="resourcePermissionChecker">The resource permission checker instance.</param>
|
|||
/// <param name="permissionName">The name of the permission to check.</param>
|
|||
/// <param name="resource">The resource for which the permission is being checked.</param>
|
|||
/// <returns>A task that represents the asynchronous operation. The task result is a boolean indicating whether the permission is granted.</returns>
|
|||
public static Task<bool> IsGrantedAsync<TResource>(this IResourcePermissionChecker resourcePermissionChecker, string permissionName, TResource resource) |
|||
where TResource : class, IKeyedObject |
|||
{ |
|||
Check.NotNull(resourcePermissionChecker, nameof(resourcePermissionChecker)); |
|||
Check.NotNullOrWhiteSpace(permissionName, nameof(permissionName)); |
|||
Check.NotNull(resource, nameof(resource)); |
|||
|
|||
return resourcePermissionChecker.IsGrantedAsync( |
|||
permissionName, |
|||
resource, |
|||
resource.GetObjectKey() ?? throw new AbpException("The resource doesn't have a key.") |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public class KeyedObjectResourcePermissionRequirementHandler : AuthorizationHandler<ResourcePermissionRequirement, IKeyedObject> |
|||
{ |
|||
protected readonly IResourcePermissionChecker PermissionChecker; |
|||
|
|||
public KeyedObjectResourcePermissionRequirementHandler(IResourcePermissionChecker permissionChecker) |
|||
{ |
|||
PermissionChecker = permissionChecker; |
|||
} |
|||
|
|||
protected override async Task HandleRequirementAsync( |
|||
AuthorizationHandlerContext context, |
|||
ResourcePermissionRequirement requirement, |
|||
IKeyedObject? resource) |
|||
{ |
|||
if (resource == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var resourceName = resource.GetType().FullName!; |
|||
var resourceKey = resource.GetObjectKey() ?? throw new AbpException("The resource doesn't have a key."); |
|||
|
|||
if (await PermissionChecker.IsGrantedAsync(context.User, requirement.PermissionName, resourceName, resourceKey)) |
|||
{ |
|||
context.Succeed(requirement); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public static class KeyedObjectResourcePermissionStoreExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Retrieves an array of granted permissions for a specific entity.
|
|||
/// </summary>
|
|||
/// <typeparam name="TResource">The type of the resource.</typeparam>
|
|||
/// <param name="resourcePermissionStore">The resource permission store instance.</param>
|
|||
/// <param name="resource">The resource for which the permissions are being checked.</param>
|
|||
/// <returns>An array of granted permission names as strings.</returns>
|
|||
public static async Task<string[]> GetGrantedPermissionsAsync<TResource>( |
|||
this IResourcePermissionStore resourcePermissionStore, |
|||
TResource resource |
|||
) |
|||
where TResource : class, IKeyedObject |
|||
{ |
|||
Check.NotNull(resourcePermissionStore, nameof(resourcePermissionStore)); |
|||
Check.NotNull(resource, nameof(resource)); |
|||
|
|||
return (await GetPermissionsAsync(resourcePermissionStore, resource)).Where(x => x.Value).Select(x => x.Key).ToArray(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Retrieves a dictionary of permissions and their granted status for the specified entity.
|
|||
/// </summary>
|
|||
/// <typeparam name="TEntity">The type of the entity.</typeparam>
|
|||
/// <param name="resourcePermissionStore">The resource permission store instance.</param>
|
|||
/// <param name="entity">The entity for which the permissions are being retrieved.</param>
|
|||
/// <returns>A dictionary where the keys are permission names and the values are booleans indicating whether the permission is granted.</returns>
|
|||
public static async Task<IDictionary<string, bool>> GetPermissionsAsync<TEntity>( |
|||
this IResourcePermissionStore resourcePermissionStore, |
|||
TEntity entity |
|||
) |
|||
where TEntity : class, IKeyedObject |
|||
{ |
|||
Check.NotNull(resourcePermissionStore, nameof(resourcePermissionStore)); |
|||
Check.NotNull(entity, nameof(entity)); |
|||
|
|||
return await resourcePermissionStore.GetPermissionsAsync( |
|||
entity, |
|||
entity.GetObjectKey() ?? throw new AbpException("The entity doesn't have a key.") |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,173 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using System.Security.Principal; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Security.Claims; |
|||
using Volo.Abp.SimpleStateChecking; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public class ResourcePermissionChecker : IResourcePermissionChecker, ITransientDependency |
|||
{ |
|||
protected IPermissionDefinitionManager PermissionDefinitionManager { get; } |
|||
protected ICurrentPrincipalAccessor PrincipalAccessor { get; } |
|||
protected ICurrentTenant CurrentTenant { get; } |
|||
protected IResourcePermissionValueProviderManager PermissionValueProviderManager { get; } |
|||
protected ISimpleStateCheckerManager<PermissionDefinition> StateCheckerManager { get; } |
|||
protected IPermissionChecker PermissionChecker { get; } |
|||
|
|||
public ResourcePermissionChecker( |
|||
ICurrentPrincipalAccessor principalAccessor, |
|||
IPermissionDefinitionManager permissionDefinitionManager, |
|||
ICurrentTenant currentTenant, |
|||
IResourcePermissionValueProviderManager permissionValueProviderManager, |
|||
ISimpleStateCheckerManager<PermissionDefinition> stateCheckerManager, |
|||
IPermissionChecker permissionChecker) |
|||
{ |
|||
PrincipalAccessor = principalAccessor; |
|||
PermissionDefinitionManager = permissionDefinitionManager; |
|||
CurrentTenant = currentTenant; |
|||
PermissionValueProviderManager = permissionValueProviderManager; |
|||
StateCheckerManager = stateCheckerManager; |
|||
PermissionChecker = permissionChecker; |
|||
} |
|||
|
|||
public virtual async Task<bool> IsGrantedAsync(string name, string resourceName, string resourceKey) |
|||
{ |
|||
return await IsGrantedAsync(PrincipalAccessor.Principal, name, resourceName, resourceKey); |
|||
} |
|||
|
|||
public virtual async Task<bool> IsGrantedAsync( |
|||
ClaimsPrincipal? claimsPrincipal, |
|||
string name, |
|||
string resourceName, |
|||
string resourceKey) |
|||
{ |
|||
Check.NotNull(name, nameof(name)); |
|||
|
|||
var permission = await PermissionDefinitionManager.GetResourcePermissionOrNullAsync(resourceName, name); |
|||
if (permission == null) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (!permission.IsEnabled) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (!await StateCheckerManager.IsEnabledAsync(permission)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var multiTenancySide = claimsPrincipal?.GetMultiTenancySide() |
|||
?? CurrentTenant.GetMultiTenancySide(); |
|||
|
|||
if (!permission.MultiTenancySide.HasFlag(multiTenancySide)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
var isGranted = false; |
|||
var context = new ResourcePermissionValueCheckContext(permission, claimsPrincipal, resourceName, resourceKey); |
|||
foreach (var provider in PermissionValueProviderManager.ValueProviders) |
|||
{ |
|||
if (context.Permission.Providers.Any() && |
|||
!context.Permission.Providers.Contains(provider.Name)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var result = await provider.CheckAsync(context); |
|||
|
|||
if (result == PermissionGrantResult.Granted) |
|||
{ |
|||
isGranted = true; |
|||
} |
|||
else if (result == PermissionGrantResult.Prohibited) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return isGranted; |
|||
} |
|||
|
|||
public async Task<MultiplePermissionGrantResult> IsGrantedAsync(string[] names, string resourceName, string resourceKey) |
|||
{ |
|||
return await IsGrantedAsync(PrincipalAccessor.Principal, names, resourceName, resourceKey); |
|||
} |
|||
|
|||
public async Task<MultiplePermissionGrantResult> IsGrantedAsync(ClaimsPrincipal? claimsPrincipal, string[] names, string resourceName, string resourceKey) |
|||
{ |
|||
Check.NotNull(names, nameof(names)); |
|||
|
|||
var result = new MultiplePermissionGrantResult(); |
|||
if (!names.Any()) |
|||
{ |
|||
return result; |
|||
} |
|||
|
|||
var multiTenancySide = claimsPrincipal?.GetMultiTenancySide() ?? |
|||
CurrentTenant.GetMultiTenancySide(); |
|||
|
|||
var permissionDefinitions = new List<PermissionDefinition>(); |
|||
foreach (var name in names) |
|||
{ |
|||
var permission = await PermissionDefinitionManager.GetResourcePermissionOrNullAsync(resourceName, name); |
|||
if (permission == null) |
|||
{ |
|||
result.Result.Add(name, PermissionGrantResult.Prohibited); |
|||
continue; |
|||
} |
|||
|
|||
result.Result.Add(name, PermissionGrantResult.Undefined); |
|||
|
|||
if (permission.IsEnabled && |
|||
await StateCheckerManager.IsEnabledAsync(permission) && |
|||
permission.MultiTenancySide.HasFlag(multiTenancySide)) |
|||
{ |
|||
permissionDefinitions.Add(permission); |
|||
} |
|||
} |
|||
|
|||
foreach (var provider in PermissionValueProviderManager.ValueProviders) |
|||
{ |
|||
var permissions = permissionDefinitions |
|||
.Where(x => !x.Providers.Any() || x.Providers.Contains(provider.Name)) |
|||
.ToList(); |
|||
|
|||
if (permissions.IsNullOrEmpty()) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var context = new ResourcePermissionValuesCheckContext( |
|||
permissions, |
|||
claimsPrincipal, |
|||
resourceName, |
|||
resourceKey); |
|||
|
|||
var multipleResult = await provider.CheckAsync(context); |
|||
foreach (var grantResult in multipleResult.Result.Where(grantResult => |
|||
result.Result.ContainsKey(grantResult.Key) && |
|||
result.Result[grantResult.Key] == PermissionGrantResult.Undefined && |
|||
grantResult.Value != PermissionGrantResult.Undefined)) |
|||
{ |
|||
result.Result[grantResult.Key] = grantResult.Value; |
|||
permissionDefinitions.RemoveAll(x => x.Name == grantResult.Key); |
|||
} |
|||
|
|||
if (result.AllGranted || result.AllProhibited) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public class ResourcePermissionPopulator : ITransientDependency |
|||
{ |
|||
protected IPermissionDefinitionManager PermissionDefinitionManager { get; } |
|||
protected IResourcePermissionChecker ResourcePermissionChecker { get; } |
|||
protected IResourcePermissionStore ResourcePermissionStore { get; } |
|||
protected IPermissionChecker PermissionChecker { get; } |
|||
|
|||
public ResourcePermissionPopulator( |
|||
IPermissionDefinitionManager permissionDefinitionManager, |
|||
IResourcePermissionChecker resourcePermissionChecker, |
|||
IResourcePermissionStore resourcePermissionStore, |
|||
IPermissionChecker permissionChecker) |
|||
{ |
|||
PermissionDefinitionManager = permissionDefinitionManager; |
|||
ResourcePermissionChecker = resourcePermissionChecker; |
|||
ResourcePermissionStore = resourcePermissionStore; |
|||
PermissionChecker = permissionChecker; |
|||
} |
|||
|
|||
public virtual async Task PopulateAsync<TResource>(TResource resource, string resourceName) |
|||
where TResource : IHasResourcePermissions |
|||
{ |
|||
await PopulateAsync([resource], resourceName); |
|||
} |
|||
|
|||
public virtual async Task PopulateAsync<TResource>(List<TResource> resources, string resourceName) |
|||
where TResource : IHasResourcePermissions |
|||
{ |
|||
Check.NotNull(resources, nameof(resources)); |
|||
Check.NotNullOrWhiteSpace(resourceName, nameof(resourceName)); |
|||
|
|||
var resopurcePermissions = (await PermissionDefinitionManager.GetResourcePermissionsAsync()) |
|||
.Where(x => x.ResourceName == resourceName) |
|||
.ToArray(); |
|||
|
|||
foreach (var resource in resources) |
|||
{ |
|||
var resourceKey = resource.GetObjectKey(); |
|||
if (resourceKey.IsNullOrEmpty()) |
|||
{ |
|||
throw new AbpException("Resource key can not be null or empty."); |
|||
} |
|||
|
|||
var results = await ResourcePermissionChecker.IsGrantedAsync(resopurcePermissions.Select(x => x.Name).ToArray(), resourceName, resourceKey); |
|||
foreach (var resopurcePermission in resopurcePermissions) |
|||
{ |
|||
if (resource.ResourcePermissions == null) |
|||
{ |
|||
ObjectHelper.TrySetProperty(resource, x => x.ResourcePermissions, () => new Dictionary<string, bool>()); |
|||
} |
|||
|
|||
var hasPermission = results.Result.TryGetValue(resopurcePermission.Name, out var granted) && granted == PermissionGrantResult.Granted; |
|||
resource.ResourcePermissions![resopurcePermission.Name] = hasPermission; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public class ResourcePermissionValueProviderManager : IResourcePermissionValueProviderManager, ISingletonDependency |
|||
{ |
|||
public IReadOnlyList<IResourcePermissionValueProvider> ValueProviders => _lazyProviders.Value; |
|||
private readonly Lazy<List<IResourcePermissionValueProvider>> _lazyProviders; |
|||
|
|||
protected AbpPermissionOptions Options { get; } |
|||
protected IServiceProvider ServiceProvider { get; } |
|||
|
|||
public ResourcePermissionValueProviderManager( |
|||
IServiceProvider serviceProvider, |
|||
IOptions<AbpPermissionOptions> options) |
|||
{ |
|||
Options = options.Value; |
|||
ServiceProvider = serviceProvider; |
|||
|
|||
_lazyProviders = new Lazy<List<IResourcePermissionValueProvider>>(GetProviders, true); |
|||
} |
|||
|
|||
protected virtual List<IResourcePermissionValueProvider> GetProviders() |
|||
{ |
|||
var providers = Options |
|||
.ResourceValueProviders |
|||
.Select(type => (ServiceProvider.GetRequiredService(type) as IResourcePermissionValueProvider)!) |
|||
.ToList(); |
|||
|
|||
var multipleProviders = providers.GroupBy(p => p.Name).FirstOrDefault(x => x.Count() > 1); |
|||
if(multipleProviders != null) |
|||
{ |
|||
throw new AbpException($"Duplicate resource permission value provider name detected: {multipleProviders.Key}. Providers:{Environment.NewLine}{multipleProviders.Select(p => p.GetType().FullName!).JoinAsString(Environment.NewLine)}"); |
|||
} |
|||
|
|||
return providers; |
|||
} |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Security.Claims; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public class RoleResourcePermissionValueProvider : ResourcePermissionValueProvider |
|||
{ |
|||
public const string ProviderName = "R"; |
|||
|
|||
public override string Name => ProviderName; |
|||
|
|||
public RoleResourcePermissionValueProvider(IResourcePermissionStore resourcePermissionStore) |
|||
: base(resourcePermissionStore) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public override async Task<PermissionGrantResult> CheckAsync(ResourcePermissionValueCheckContext context) |
|||
{ |
|||
var roles = context.Principal?.FindAll(AbpClaimTypes.Role).Select(c => c.Value).ToArray(); |
|||
|
|||
if (roles == null || !roles.Any()) |
|||
{ |
|||
return PermissionGrantResult.Undefined; |
|||
} |
|||
|
|||
foreach (var role in roles.Distinct()) |
|||
{ |
|||
if (await ResourcePermissionStore.IsGrantedAsync(context.Permission.Name, context.ResourceName, context.ResourceKey, Name, role)) |
|||
{ |
|||
return PermissionGrantResult.Granted; |
|||
} |
|||
} |
|||
|
|||
return PermissionGrantResult.Undefined; |
|||
} |
|||
|
|||
public override async Task<MultiplePermissionGrantResult> CheckAsync(ResourcePermissionValuesCheckContext context) |
|||
{ |
|||
var permissionNames = context.Permissions.Select(x => x.Name).Distinct().ToList(); |
|||
Check.NotNullOrEmpty(permissionNames, nameof(permissionNames)); |
|||
|
|||
var result = new MultiplePermissionGrantResult(permissionNames.ToArray()); |
|||
|
|||
var roles = context.Principal?.FindAll(AbpClaimTypes.Role).Select(c => c.Value).ToArray(); |
|||
if (roles == null || !roles.Any()) |
|||
{ |
|||
return result; |
|||
} |
|||
|
|||
foreach (var role in roles.Distinct()) |
|||
{ |
|||
var multipleResult = await ResourcePermissionStore.IsGrantedAsync(permissionNames.ToArray(), context.ResourceName, context.ResourceKey, Name, role); |
|||
|
|||
foreach (var grantResult in multipleResult.Result.Where(grantResult => |
|||
result.Result.ContainsKey(grantResult.Key) && |
|||
result.Result[grantResult.Key] == PermissionGrantResult.Undefined && |
|||
grantResult.Value != PermissionGrantResult.Undefined)) |
|||
{ |
|||
result.Result[grantResult.Key] = grantResult.Value; |
|||
permissionNames.RemoveAll(x => x == grantResult.Key); |
|||
} |
|||
|
|||
if (result.AllGranted || result.AllProhibited) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
if (permissionNames.IsNullOrEmpty()) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Security.Claims; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions.Resources; |
|||
|
|||
public class UserResourcePermissionValueProvider : ResourcePermissionValueProvider |
|||
{ |
|||
public const string ProviderName = "U"; |
|||
|
|||
public override string Name => ProviderName; |
|||
|
|||
public UserResourcePermissionValueProvider(IResourcePermissionStore resourcePermissionStore) |
|||
: base(resourcePermissionStore) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public override async Task<PermissionGrantResult> CheckAsync(ResourcePermissionValueCheckContext context) |
|||
{ |
|||
var userId = context.Principal?.FindFirst(AbpClaimTypes.UserId)?.Value; |
|||
|
|||
if (userId == null) |
|||
{ |
|||
return PermissionGrantResult.Undefined; |
|||
} |
|||
|
|||
return await ResourcePermissionStore.IsGrantedAsync(context.Permission.Name, context.ResourceName, context.ResourceKey, Name, userId) |
|||
? PermissionGrantResult.Granted |
|||
: PermissionGrantResult.Undefined; |
|||
} |
|||
|
|||
public override async Task<MultiplePermissionGrantResult> CheckAsync(ResourcePermissionValuesCheckContext context) |
|||
{ |
|||
var permissionNames = context.Permissions.Select(x => x.Name).Distinct().ToArray(); |
|||
Check.NotNullOrEmpty(permissionNames, nameof(permissionNames)); |
|||
|
|||
var userId = context.Principal?.FindFirst(AbpClaimTypes.UserId)?.Value; |
|||
if (userId == null) |
|||
{ |
|||
return new MultiplePermissionGrantResult(permissionNames); |
|||
} |
|||
|
|||
return await ResourcePermissionStore.IsGrantedAsync(permissionNames, context.ResourceName, context.ResourceKey, Name, userId); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Internal.Telemetry; |
|||
using Volo.Abp.Internal.Telemetry.Activity; |
|||
|
|||
namespace Volo.Abp.Cli.Telemetry; |
|||
|
|||
public class NullTelemetryService : ITelemetryService |
|||
{ |
|||
public IAsyncDisposable TrackActivity(ActivityEvent activityData) |
|||
{ |
|||
return NullAsyncDisposable.Instance; |
|||
} |
|||
public IAsyncDisposable TrackActivityAsync(string activityName, Action<Dictionary<string, object>>? additionalProperties = null) |
|||
{ |
|||
return NullAsyncDisposable.Instance; |
|||
} |
|||
|
|||
public Task AddActivityAsync(string activityName, Action<Dictionary<string, object>>? additionalProperties = null) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public Task AddErrorActivityAsync(Action<Dictionary<string, object>> additionalProperties) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public Task AddErrorActivityAsync(string errorMessage) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public Task AddErrorForActivityAsync(string failingActivity, string errorMessage) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Internal.Telemetry.Activity; |
|||
using Volo.Abp.Internal.Telemetry.Activity.Contracts; |
|||
using Volo.Abp.Internal.Telemetry.Activity.Providers; |
|||
using Volo.Abp.Internal.Telemetry.Constants; |
|||
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
|||
|
|||
namespace Volo.Abp.Cli.Telemetry; |
|||
|
|||
[ExposeServices(typeof(ITelemetryActivityEventEnricher))] |
|||
public class TelemetryCliSessionProvider : TelemetryActivityEventEnricher |
|||
{ |
|||
public TelemetryCliSessionProvider(IServiceProvider serviceProvider) : base(serviceProvider) |
|||
{ |
|||
} |
|||
|
|||
public override int ExecutionOrder { get; set; } = 10; |
|||
|
|||
protected override Task ExecuteAsync(ActivityContext context) |
|||
{ |
|||
context.Current[ActivityPropertyNames.SessionType] = SessionType.AbpCli; |
|||
context.Current[ActivityPropertyNames.SessionId] = Guid.NewGuid(); |
|||
context.Current[ActivityPropertyNames.IsFirstSession] = !File.Exists(TelemetryPaths.ActivityStorage); |
|||
context.Current["OldCli"] = true; |
|||
|
|||
if(context.Current.TryGetValue<Dictionary<string, object>>(ActivityPropertyNames.AdditionalProperties, out var additionalProperties)) |
|||
{ |
|||
additionalProperties["OldCli"] = true; |
|||
} |
|||
else |
|||
{ |
|||
context.Current[ActivityPropertyNames.AdditionalProperties] = new Dictionary<string, object> |
|||
{ |
|||
{ "OldCli", true } |
|||
}; |
|||
} |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Volo.Abp; |
|||
|
|||
public interface IKeyedObject |
|||
{ |
|||
string? GetObjectKey(); |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
|
|||
namespace Volo.Abp; |
|||
|
|||
public static class KeyedObjectHelper |
|||
{ |
|||
public static string EncodeCompositeKey(params object?[] keys) |
|||
{ |
|||
var raw = keys.JoinAsString("||"); |
|||
var bytes = Encoding.UTF8.GetBytes(raw); |
|||
var base64 = Convert.ToBase64String(bytes); |
|||
var base64Url = base64 |
|||
.Replace("+", "-") |
|||
.Replace("/", "_") |
|||
.TrimEnd('='); |
|||
|
|||
return base64Url; |
|||
} |
|||
|
|||
public static string DecodeCompositeKey(string encoded) |
|||
{ |
|||
var base64 = encoded |
|||
.Replace("-", "+") |
|||
.Replace("_", "/"); |
|||
|
|||
switch (encoded.Length % 4) |
|||
{ |
|||
case 2: base64 += "=="; break; |
|||
case 3: base64 += "="; break; |
|||
} |
|||
|
|||
var bytes = Convert.FromBase64String(base64); |
|||
var raw = Encoding.UTF8.GetString(bytes); |
|||
|
|||
return raw; |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue