@ -1,54 +1,79 @@ |
|||
import os |
|||
import json |
|||
import re |
|||
import xml.etree.ElementTree as ET |
|||
from github import Github |
|||
|
|||
def update_latest_versions(): |
|||
version = os.environ["GITHUB_REF"].split("/")[-1] |
|||
def get_target_release_branch(version): |
|||
""" |
|||
Extracts the first two numbers from the release version (`9.0.5` → `rel-9.0`) |
|||
to determine the corresponding `rel-x.x` branch. |
|||
""" |
|||
match = re.match(r"(\d+)\.(\d+)\.\d+", version) |
|||
if not match: |
|||
raise ValueError(f"Invalid version format: {version}") |
|||
|
|||
if "rc" in version: |
|||
return False |
|||
major, minor = match.groups() |
|||
target_branch = f"rel-{major}.{minor}" |
|||
return target_branch |
|||
|
|||
with open("latest-versions.json", "r") as f: |
|||
latest_versions = json.load(f) |
|||
def get_version_from_common_props(branch): |
|||
""" |
|||
Retrieves `Version` and `LeptonXVersion` from the `common.props` file in the specified branch. |
|||
""" |
|||
g = Github(os.environ["GITHUB_TOKEN"]) |
|||
repo = g.get_repo("abpframework/abp") |
|||
|
|||
latest_versions[0]["version"] = version |
|||
try: |
|||
file_content = repo.get_contents("common.props", ref=branch) |
|||
common_props_content = file_content.decoded_content.decode("utf-8") |
|||
|
|||
with open("latest-versions.json", "w") as f: |
|||
json.dump(latest_versions, f, indent=2) |
|||
root = ET.fromstring(common_props_content) |
|||
version = root.find(".//Version").text |
|||
leptonx_version = root.find(".//LeptonXVersion").text |
|||
|
|||
return True |
|||
return version, leptonx_version |
|||
except Exception as e: |
|||
raise FileNotFoundError(f"common.props not found in branch {branch}: {e}") |
|||
|
|||
def create_pr(): |
|||
g = Github(os.environ["GITHUB_TOKEN"]) |
|||
repo = g.get_repo("abpframework/abp") |
|||
def update_latest_versions(): |
|||
""" |
|||
Updates `latest-versions.json` based on the most relevant release branch. |
|||
""" |
|||
# Get the release version from GitHub reference |
|||
release_version = os.environ["GITHUB_REF"].split("/")[-1] # Example: "refs/tags/v9.0.5" → "v9.0.5" |
|||
if release_version.startswith("v"): |
|||
release_version = release_version[1:] # Convert to "9.0.5" format |
|||
|
|||
branch_name = f"update-latest-versions-{os.environ['GITHUB_REF'].split('/')[-1]}" |
|||
base = repo.get_branch("dev") |
|||
repo.create_git_ref(ref=f"refs/heads/{branch_name}", sha=base.commit.sha) |
|||
# Determine the correct `rel-x.x` branch |
|||
target_branch = get_target_release_branch(release_version) |
|||
|
|||
# Retrieve `common.props` data from the target branch |
|||
version, leptonx_version = get_version_from_common_props(target_branch) |
|||
|
|||
# Get the current latest-versions.json file and its sha |
|||
contents = repo.get_contents("latest-versions.json", ref="dev") |
|||
file_sha = contents.sha |
|||
# Skip if the version is a preview or release candidate |
|||
if "preview" in version or "rc" in version: |
|||
return False |
|||
|
|||
# Update the file in the repo |
|||
repo.update_file( |
|||
path="latest-versions.json", |
|||
message=f"Update latest-versions.json to version {os.environ['GITHUB_REF'].split('/')[-1]}", |
|||
content=open("latest-versions.json", "r").read().encode("utf-8"), |
|||
sha=file_sha, |
|||
branch=branch_name, |
|||
) |
|||
# Read the `latest-versions.json` file |
|||
with open("latest-versions.json", "r") as f: |
|||
latest_versions = json.load(f) |
|||
|
|||
try: |
|||
pr = repo.create_pull(title="Update latest-versions.json", |
|||
body="Automated PR to update the latest-versions.json file.", |
|||
head=branch_name, base="dev") |
|||
except Exception as e: |
|||
print(f"Error while creating PR: {e}") |
|||
# Add the new version entry |
|||
new_version_entry = { |
|||
"version": version, |
|||
"releaseDate": "", |
|||
"type": "stable", |
|||
"message": "", |
|||
"leptonx": { |
|||
"version": leptonx_version |
|||
} |
|||
} |
|||
|
|||
latest_versions.insert(0, new_version_entry) # Insert the new version at the top |
|||
|
|||
pr.create_review_request(reviewers=["ebicoglu", "gizemmutukurt", "skoc10"]) |
|||
# Update the file |
|||
with open("latest-versions.json", "w") as f: |
|||
json.dump(latest_versions, f, indent=2) |
|||
|
|||
if __name__ == "__main__": |
|||
should_create_pr = update_latest_versions() |
|||
if should_create_pr: |
|||
create_pr() |
|||
return True |
|||
|
|||
@ -0,0 +1,147 @@ |
|||
# ABP Platform 9.1 RC Has Been Released |
|||
|
|||
We are happy to release [ABP](https://abp.io) version **9.1 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version. |
|||
|
|||
Try this version and provide feedback for a more stable version of ABP v9.1! Thanks to you in advance. |
|||
|
|||
## Get Started with the 9.1 RC |
|||
|
|||
You can check the [Get Started page](https://abp.io/get-started) to see how to get started with ABP. You can either download [ABP Studio](https://abp.io/get-started#abp-studio-tab) (**recommended**, if you prefer a user-friendly GUI application - desktop application) or use the [ABP CLI](https://abp.io/docs/latest/cli). |
|||
|
|||
By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI: |
|||
|
|||
 |
|||
|
|||
## Migration Guide |
|||
|
|||
There are no breaking changes in this version that would affect your application. Only you might need to update some constant names due to the OpenIddict 6.0 upgrade, which is explained in the [OpenIddict 6.0 migration guide](https://abp.io/docs/9.1/release-info/migration-guides/openiddict5-to-6). |
|||
|
|||
## What's New with ABP v9.1? |
|||
|
|||
In this section, I will introduce some major features released in this version. |
|||
Here is a brief list of titles explained in the next sections: |
|||
|
|||
* Upgraded to Angular 19 |
|||
* Upgraded to OpenIddict 6.0 |
|||
* New Blazor WASM Bundling System |
|||
* Idle Session Warning |
|||
* Lazy Expandable Feature for Documentation |
|||
|
|||
### Upgraded to Angular 19 |
|||
|
|||
We've upgraded the Angular templates and packages to **Angular 19**. This upgrade brings the latest features and improvements from the Angular ecosystem to ABP-based applications, including better performance and development experience. |
|||
|
|||
### Upgraded to OpenIddict 6.0 |
|||
|
|||
OpenIddict 6.0 has been released and we've upgraded the OpenIddict packages to version 6.0 in ABP 9.1. This brings enhanced security features and improved authentication capabilities. The migration is straightforward and mainly involves updating some constant names: |
|||
|
|||
- `OpenIddictConstants.Permissions.Endpoints.Logout` is now `OpenIddictConstants.Permissions.Endpoints.EndSession` |
|||
- `OpenIddictConstants.Permissions.Endpoints.Device` is now `OpenIddictConstants.Permissions.Endpoints.DeviceAuthorization` |
|||
|
|||
If you're using IdentityModel packages directly, you'll need to upgrade them to the latest stable version (8.3.0). This update ensures your applications stay current with the latest security standards and best practices. |
|||
|
|||
> Please refer to the [OpenIddict 6.0 migration guide](https://abp.io/docs/9.1/release-info/migration-guides/openiddict5-to-6) for more information. |
|||
|
|||
### New Blazor WASM Bundling System |
|||
|
|||
We've implemented a new bundling system for Blazor WebAssembly applications that eliminates the need to manually run the `abp bundle` command. This system automatically handles JavaScript and CSS file bundling at runtime, significantly improving both development experience and application loading performance. |
|||
|
|||
**Key improvements include:** |
|||
|
|||
- Automatic bundling of JavaScript and CSS files without manual intervention |
|||
- Dynamic file generation through the host application |
|||
- Better integration with the ABP module system |
|||
- Improved asset management through virtual file system |
|||
|
|||
The new system is particularly beneficial for modular applications, as it allows modules to contribute their assets automatically to the global bundles. This results in a more maintainable and efficient asset management system for Blazor WebAssembly applications. |
|||
|
|||
> Please refer to [this documentation](https://abp.io/docs/9.1/framework/ui/blazor/global-scripts-styles) for more information. |
|||
|
|||
### Idle Session Warning |
|||
|
|||
We've introduced a new idle session warning feature for the [Account (Pro) Module](https://abp.io/docs/latest/modules/account-pro) that helps manage user sessions more effectively. This security enhancement automatically monitors user activity and manages session timeouts in a user-friendly way. |
|||
|
|||
 |
|||
|
|||
The feature can be easily configured through the administration interface, where administrators can: |
|||
|
|||
- Enable/disable the idle session timeout |
|||
- Set custom timeout duration in minutes |
|||
- Configure when users should be signed out |
|||
|
|||
When a user becomes inactive for the configured duration, they'll receive a warning dialog: |
|||
|
|||
 |
|||
|
|||
**Key features and behaviors:** |
|||
|
|||
- Tracks real user activity (mouse movements, keyboard presses) across all tabs |
|||
- Works on a per-browser session basis - affects all tabs of the same session |
|||
- Maintains session if user is active in any tab of the application |
|||
- Provides a countdown timer before automatic sign-out |
|||
- Offers options to "Stay signed in" or "Sign out now" |
|||
|
|||
This feature significantly improves application security while maintaining a smooth user experience by preventing unexpected session expirations and data loss. |
|||
|
|||
### Lazy Expandable Feature for Documentation |
|||
|
|||
We've introduced a new lazy expandable feature to the documentation system that significantly improves navigation through large documentation sections. This enhancement addresses common challenges when dealing with extensive documentation hierarchies by introducing smart menu management. |
|||
|
|||
**Key benefits and features:** |
|||
|
|||
- **Cleaner Navigation:** The menu stays concise by hiding sub-items until they're needed, reducing visual clutter |
|||
- **Better Performance:** Reduces the initial load of the navigation tree by loading sub-items on demand |
|||
- **Improved Search Experience:** Makes filtering documentation items more efficient by showing only relevant top-level items |
|||
- **Context-Aware Expansion:** Automatically expands relevant sections when viewing specific documentation pages |
|||
|
|||
The feature works by marking certain documentation sections as "lazy expandable" in the navigation configuration. When users navigate to a document within a lazy expandable section, the system automatically expands the relevant menu items while keeping other sections collapsed. |
|||
|
|||
This improvement is particularly valuable for complex documentation areas like tutorials, solution templates, and extensive module documentation, where having all navigation items visible at once could be overwhelming. |
|||
|
|||
An example of lazy expandable feature from the [ABP's BookStore Tutorial](https://abp.io/docs/latest/tutorials/book-store/part-01): |
|||
|
|||
```json |
|||
{ |
|||
"text": "Book Store Application", |
|||
"isLazyExpandable": true, |
|||
"path": "tutorials/book-store", |
|||
"items": [ |
|||
{ |
|||
"text": "Overview", |
|||
"path": "tutorials/book-store", |
|||
"isIndex": true |
|||
}, |
|||
//other items... |
|||
] |
|||
} |
|||
``` |
|||
|
|||
 |
|||
|
|||
### Others |
|||
|
|||
Some other highlights from this release: |
|||
|
|||
* Updated Iyzico NuGet packages to the latest version, which is used in the [ABP's Payment Module](https://abp.io/docs/latest/modules/payment#payment-module-pro). |
|||
* Removed optional _secondaryIds_ from path. See: [#21307](https://github.com/abpframework/abp/pull/21307) |
|||
* [CMS Kit Pro](https://abp.io/docs/latest/modules/cms-kit-pro): Added automatic deletion of comments when a blog post is deleted - comments are now automatically removed when their associated blog post is deleted. |
|||
* Avoiding global blocking in distributed event handlers (See [#21716](https://github.com/abpframework/abp/pull/21716)). |
|||
|
|||
## Community News |
|||
|
|||
### New ABP Community Articles |
|||
|
|||
There are exciting articles contributed by the ABP community as always. I will highlight some of them here: |
|||
|
|||
* [Integrating ABP Modules in Your ASP.NET Core Web API Project. A Step-by-Step Guide](https://abp.io/community/articles/integrating-abp-modules-in-your-asp.net-core-web-api-project.-a-stepbystep-guide-jtbyosnr) by [Sajankumar Vijayan](https://abp.io/community/members/connect) |
|||
* [ABP Framework: Background Jobs vs Background Workers](https://abp.io/community/articles/abp-framework-background-jobs-vs-background-workers-when-to-use-which-t98pzjv6) — When to Use Which? by [Alper Ebiçoğlu](https://twitter.com/alperebicoglu) |
|||
* [The new Unit Test structure in ABP application](https://abp.io/community/articles/the-new-unit-test-structure-in-abp-application-4vvvp2oy) by [Liming Ma](https://github.com/maliming) |
|||
* [How to Use OpenAI API with ABP Framework](https://abp.io/community/articles/how-to-use-openai-api-with-abp-framework-rsfvihla) by [Berkan Şaşmaz](https://github.com/berkansasmaz) |
|||
|
|||
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/submit) to the ABP Community. |
|||
|
|||
## Conclusion |
|||
|
|||
This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://abp.io/docs/9.1/release-info/road-map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v9.1 RC and provide feedback to help us release a more stable version. |
|||
|
|||
Thanks for being a part of this community! |
|||
|
After Width: | Height: | Size: 482 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 34 KiB |
@ -0,0 +1,46 @@ |
|||
# ABP Studio Now Supports MacOS Intel 🚀 |
|||
|
|||
We are excited to announce that [ABP Studio, our cross-platform desktop application for ABP developers](https://abp.io/studio), now supports Intel-based Mac computers! |
|||
|
|||
This addition expands our platform compatibility, ensuring that developers using Intel-powered Macs can also benefit from the powerful features of ABP Studio. |
|||
|
|||
## What is ABP Studio? |
|||
|
|||
For those who aren't familiar, [ABP Studio](https://abp.io/studio) is a powerful desktop application that makes ABP development faster and easier. It offers: |
|||
|
|||
* Easy creation of new solutions (from simple applications to microservices) |
|||
* Visual architecture management for modular-monolith and microservice solutions |
|||
* Solution exploration tools for entities, services, and HTTP APIs |
|||
* Simplified running, debugging and monitoring of multi-application or microservice solutions |
|||
* Kubernetes cluster integration capabilities |
|||
* and more... |
|||
|
|||
## Extended Platform Support |
|||
|
|||
ABP Studio has been proudly supporting multiple platforms, and we're excited to add MacOS Intel to the our list of supported architectures. You can now use ABP Studio on: |
|||
|
|||
* Windows x64 |
|||
* Windows ARM |
|||
* MacOS Apple Silicon (M1/M2/M3) |
|||
* MacOS Intel **(New!)** |
|||
|
|||
## Why This Matters |
|||
|
|||
This update is particularly important for developers who are using Intel-based Mac computers. Previously, ABP Studio was only available for Apple Silicon Macs (for MacOS), but we understand that many developers are still using Intel-based Macs. With this release, we're ensuring that all Mac users can access our development tools, regardless of their processor architecture. |
|||
|
|||
## Getting Started |
|||
|
|||
Installing ABP Studio on your Intel-based Mac is straightforward: |
|||
|
|||
1. Go to [abp.io/studio](https://abp.io/studio) |
|||
2. Click on the download button and select "MacOS Intel" from the dropdown menu |
|||
3. Once downloaded, open the installer package |
|||
4. Follow the installation wizard to complete the setup |
|||
|
|||
 |
|||
|
|||
## Conclusion |
|||
|
|||
As ABP team, we're always looking for ways to improve the developer experience. By supporting Intel-based Macs, we're ensuring that all Mac users can access our development tools, regardless of their processor architecture. |
|||
|
|||
Stay tuned for more updates and enhancements as we continue to optimize ABP Studio and please provide us with your invaluable feedback. Thanks in advance! |
|||
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 466 KiB |
@ -0,0 +1,407 @@ |
|||
# ABP Framework: Background Jobs vs Background Workers — When to Use Which? |
|||
|
|||
In the ABP Framework, **Background Jobs** and **Background Workers** serve different purposes but can sometimes seem interchangeable. Sometimes it can be confusing. Let’s clarify their differences, and I'll show you some real-world cases to help you understand how to decide between these two. We have official documents for these: |
|||
|
|||
📕 **Background Workers ABP Document** https://abp.io/docs/latest/framework/infrastructure/background-workers |
|||
|
|||
📘 **Background Jobs ABP Document** https://abp.io/docs/latest/framework/infrastructure/background-jobs |
|||
|
|||
|
|||
--- |
|||
|
|||
I posted this article because recently, I came across [this support ticket](https://abp.io/support/questions/5931/Background-Jobs-vs-Background-Workers-when-to-use-which) on the ABP support website. I thought these terms could be confusing for devs. He has two tasks and is asking whether he needs to use Background Worker or Background Job. |
|||
|
|||
* The first one is `FileRecievedActivity.` He says it's running 2 times a day. So, it's a recurring activity that needs to run in the background. So `FileRecievedActivity` is a "Background Worker". |
|||
* The second one is `ProcessQueueFiles`. It sounds like a polling task. It runs whenever a new file comes to the directory. So, it's also a recurring task that needs to be scheduled every 1 minute. Hence the `ProcessQueueFiles` is also "Backgrounder Worker." |
|||
|
|||
 |
|||
|
|||
|
|||
|
|||
## Background Workers 🔄 */Looping/* |
|||
|
|||
The background workers are stateless and runs in-memory as long as the application is up and ready. |
|||
|
|||
- **Purpose**: Long-running, periodic tasks that run continuously in the background. They are queued and executed asynchronously. |
|||
- **Lifetime:** Runs throughout the application's lifetime. |
|||
- **Scheduling:** Executes on a fixed schedule, like every X minutes / hours... |
|||
- **Infrastructure**: ABP uses an in-memory implementation for running background workers. You can also use the 3rd party tools for running your background worker: |
|||
* [Quartz + ABP integration](https://abp.io/docs/latest/framework/infrastructure/background-workers/quartz) |
|||
* [Hangfire + ABP integration](https://abp.io/docs/latest/framework/infrastructure/background-workers/hangfire) |
|||
|
|||
|
|||
|
|||
**Use Cases:** Use background workers for any task that needs to run repeatedly at fixed intervals. For example "Health checks", "Periodic cleanup tasks", "Monitoring tasks", "Processing daily data"... In the last section, you will find real-world examples. |
|||
|
|||
|
|||
|
|||
> In Microsoft Docs, this topic is called "Background Tasks". Check out [Microsoft's official doc for running background tasks](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services). |
|||
|
|||
|
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
## Background Jobs ▶ */One time/* |
|||
|
|||
* **Purpose:** One-time tasks that need to be queued and can be executed in the background. Here the main goal is not to block the application to run the task. |
|||
|
|||
- **Lifetime**: Executes one time and completes. |
|||
|
|||
- **Scheduling**: Can be delayed/scheduled for a specific time. |
|||
|
|||
- **Infrastructure**: ABP uses in-memory implementation but you can also use the below 3rd party tools: |
|||
|
|||
* [Hangfire + ABP implementation](https://abp.io/docs/latest/framework/infrastructure/background-jobs/hangfire) |
|||
|
|||
* [RabbitMQ + ABP implementation](https://abp.io/docs/latest/framework/infrastructure/background-jobs/rabbitmq) |
|||
|
|||
* [Quartz + ABP implementation](https://abp.io/docs/latest/framework/infrastructure/background-jobs/quartz) |
|||
|
|||
|
|||
|
|||
**Use Cases:** Use background jobs to send emails, process uploaded files, generating reports, any fire-and-forget tasks or tasks that need retry mechanisms. |
|||
|
|||
|
|||
|
|||
> In Microsoft docs this topic is called as "Background Tasks". Check out [Microsoft's official background jobs doc](https://learn.microsoft.com/en-us/azure/architecture/best-practices/background-jobs). |
|||
|
|||
|
|||
|
|||
## When to Use Which? |
|||
|
|||
### Use Background Workers when: |
|||
|
|||
- You need continuous, recurring execution |
|||
|
|||
- The task should run as long as the application is running |
|||
|
|||
- You don't need persistence in task state |
|||
|
|||
- You want in-memory, efficient execution |
|||
|
|||
|
|||
|
|||
### Use Background Jobs when: |
|||
|
|||
- You need one-time execution |
|||
|
|||
- The task should survive application restarts |
|||
|
|||
- You need guaranteed execution |
|||
|
|||
- You want built-in retry mechanisms |
|||
|
|||
- You need to queue multiple instances of the same task |
|||
|
|||
|
|||
|
|||
## Technical Differences |
|||
|
|||
```csharp |
|||
// A Background Worker Example |
|||
public class MyBackgroundWorker : AsyncPeriodicBackgroundWorkerBase |
|||
{ |
|||
public MyBackgroundWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory) |
|||
: base(timer, serviceScopeFactory) |
|||
{ |
|||
Timer.Period = 5000; // Runs every 5 seconds |
|||
} |
|||
} |
|||
|
|||
// A Background Job Example |
|||
public class MyBackgroundJob : AsyncBackgroundJob<MyArgs> |
|||
{ |
|||
public override async Task ExecuteAsync(MyArgs args) |
|||
{ |
|||
// Executes once when triggered |
|||
} |
|||
} |
|||
|
|||
// Triggering a Background Job |
|||
await _backgroundJobManager.EnqueueAsync( |
|||
new MyArgs { /* ... */ }, |
|||
delay: TimeSpan.FromHours(2) |
|||
); |
|||
``` |
|||
|
|||
|
|||
|
|||
|
|||
> While background workers no additional infrastructure needed, background jobs requires a database to persist jobs (or a distributed cache/message broker depending on the implementation) |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
## 🚩 Background Worker Examples |
|||
|
|||
|
|||
|
|||
### 1) Querying an external API |
|||
|
|||
- **Scenario**: Continuously fetch data from an external API at regular intervals. |
|||
- **Why a Background Worker?**: You need a long-running process to poll the API and handle the results. |
|||
|
|||
```csharp |
|||
public class ApiPollingWorker : AsyncPeriodicBackgroundWorkerBase |
|||
{ |
|||
private readonly IAmazonPriceService _apiService; |
|||
|
|||
public ApiPollingWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory, IAmazonPriceService apiService) |
|||
: base(timer, serviceScopeFactory) |
|||
{ |
|||
_apiService = apiService; |
|||
Timer.Period = 60000; // Run every 1 minute |
|||
} |
|||
|
|||
protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) |
|||
{ |
|||
var data = await _apiService.FetchPricesAsync(); |
|||
// Process the data |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Here are practical examples of **Background Workers** that demonstrate how to use them effectively within the ABP Framework. Background Workers are ideal for long-running or periodic tasks that require stateful or persistent execution. |
|||
|
|||
|
|||
|
|||
### 2) Polling an External API |
|||
|
|||
- **Scenario**: Continuously fetch data from an external API at regular intervals. |
|||
- **Why a Background Worker?**: You need a long-running process to poll the API and handle the results. |
|||
|
|||
``` |
|||
csharpCopy codepublic class ApiPollingWorker : AsyncPeriodicBackgroundWorkerBase |
|||
{ |
|||
private readonly IApiService _apiService; |
|||
|
|||
public ApiPollingWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory, IApiService apiService) |
|||
: base(timer, serviceScopeFactory) |
|||
{ |
|||
_apiService = apiService; |
|||
Timer.Period = 60000; // Run every 1 minute |
|||
} |
|||
|
|||
protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) |
|||
{ |
|||
var data = await _apiService.FetchDataAsync(); |
|||
// Process the data |
|||
} |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
### 3) Processing a Queue |
|||
|
|||
- **Scenario**: Continuously process items from a queue, such as Azure Service Bus or RabbitMQ. |
|||
- **Why a Background Worker?**: This requires a stateful, persistent process to monitor and handle the queue. |
|||
|
|||
```csharp |
|||
public class QueueProcessingWorker : AsyncBackgroundWorker |
|||
{ |
|||
private readonly IQueueService _queueService; |
|||
|
|||
public QueueProcessingWorker(IServiceScopeFactory serviceScopeFactory, IQueueService queueService) |
|||
: base(serviceScopeFactory) |
|||
{ |
|||
_queueService = queueService; |
|||
} |
|||
|
|||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) |
|||
{ |
|||
while (!stoppingToken.IsCancellationRequested) |
|||
{ |
|||
var message = await _queueService.GetNextMessageAsync(); |
|||
if (message != null) |
|||
{ |
|||
await _queueService.ProcessMessageAsync(message); |
|||
} |
|||
await Task.Delay(1000, stoppingToken); // Delay for throttling |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
### 4) Maintaining a Database |
|||
|
|||
- **Scenario**: Perform periodic database maintenance, rebuild indices or cleanup tasks. |
|||
- **Why a Background Worker?**: These are recurring tasks requiring regular execution. |
|||
|
|||
```csharp |
|||
public class DatabaseCleanupWorker : AsyncPeriodicBackgroundWorkerBase |
|||
{ |
|||
private readonly ICleanupService _cleanupService; |
|||
|
|||
public DatabaseCleanupWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory, ICleanupService cleanupService) |
|||
: base(timer, serviceScopeFactory) |
|||
{ |
|||
_cleanupService = cleanupService; |
|||
Timer.Period = 3600000; // Run every hour |
|||
} |
|||
|
|||
protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) |
|||
{ |
|||
await _cleanupService.DeleteOldRecordsAsync(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
### 5) Running a Health Check Service |
|||
|
|||
- **Scenario**: Periodically check the health of connected services and log their status. |
|||
- **Why a Background Worker?**: Periodic health checks are naturally suited to background workers. |
|||
|
|||
```csharp |
|||
public class HealthCheckWorker : AsyncPeriodicBackgroundWorkerBase |
|||
{ |
|||
private readonly IHealthCheckService _healthCheckService; |
|||
|
|||
public HealthCheckWorker(AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory, IHealthCheckService healthCheckService) |
|||
: base(timer, serviceScopeFactory) |
|||
{ |
|||
_healthCheckService = healthCheckService; |
|||
Timer.Period = 60000; // Run every minute |
|||
} |
|||
|
|||
protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) |
|||
{ |
|||
await _healthCheckService.PerformChecksAsync(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
## 🚩 Background Job Examples |
|||
|
|||
|
|||
|
|||
### 1) Email Notifications |
|||
|
|||
- **Scenario**: Sending a welcome email to new users. |
|||
- **Why a Background Job?**: The email sending process doesn’t need to block the main application thread. It can run in the background, and failed jobs can be retried. |
|||
|
|||
```csharp |
|||
public class SendWelcomeEmailJob : IBackgroundJob<string> |
|||
{ |
|||
private readonly IEmailSender _emailSender; |
|||
|
|||
public SendWelcomeEmailJob(IEmailSender emailSender) |
|||
{ |
|||
_emailSender = emailSender; |
|||
} |
|||
|
|||
public async Task ExecuteAsync(string emailAddress) |
|||
{ |
|||
await _emailSender.SendAsync( |
|||
emailAddress, |
|||
"Welcome!", |
|||
"Thank you for signing up to our service." |
|||
); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
### 2) Data Import/Export |
|||
|
|||
- **Scenario**: Exporting a large dataset to a CSV file and notifying the user once complete. |
|||
- **Why a Background Job?**: Long-running tasks like file generation can be processed asynchronously. |
|||
|
|||
```csharp |
|||
public class ExportDataJob : IBackgroundJob<int> |
|||
{ |
|||
private readonly IDataExporter _dataExporter; |
|||
private readonly INotificationService _notificationService; |
|||
|
|||
public ExportDataJob(IDataExporter dataExporter, INotificationService notificationService) |
|||
{ |
|||
_dataExporter = dataExporter; |
|||
_notificationService = notificationService; |
|||
} |
|||
|
|||
public async Task ExecuteAsync(int userId) |
|||
{ |
|||
var filePath = await _dataExporter.ExportAsync(userId); |
|||
await _notificationService.NotifyAsync(userId, "Your data export is complete: " + filePath); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
### 3) Push Notifications |
|||
|
|||
- **Scenario**: Sending push notifications to users about specific events (e.g., “Your order has been shipped!”). |
|||
- **Why a Background Job?**: Push notifications are non-blocking and can be handled in bulk in the background. |
|||
|
|||
```csharp |
|||
public class SendPushNotificationJob : IBackgroundJob<PushNotificationInput> |
|||
{ |
|||
private readonly IPushNotificationService _pushNotificationService; |
|||
|
|||
public SendPushNotificationJob(IPushNotificationService pushNotificationService) |
|||
{ |
|||
_pushNotificationService = pushNotificationService; |
|||
} |
|||
|
|||
public async Task ExecuteAsync(PushNotificationInput input) |
|||
{ |
|||
await _pushNotificationService.SendAsync(input.UserId, input.Message); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
### 4) File Upload Processing |
|||
|
|||
- **Scenario**: Processing a file uploaded by a user (e.g., parsing, validating, and saving to a database). |
|||
- **Why a Background Job?**: File processing can be offloaded to the background, ensuring quick user feedback. |
|||
|
|||
```csharp |
|||
public class ProcessFileUploadJob : IBackgroundJob<FileProcessingInput> |
|||
{ |
|||
private readonly IFileParser _fileParser; |
|||
private readonly IDataService _dataService; |
|||
|
|||
public ProcessFileUploadJob(IFileParser fileParser, IDataService dataService) |
|||
{ |
|||
_fileParser = fileParser; |
|||
_dataService = dataService; |
|||
} |
|||
|
|||
public async Task ExecuteAsync(FileProcessingInput input) |
|||
{ |
|||
var parsedData = await _fileParser.ParseAsync(input.FilePath); |
|||
await _dataService.SaveAsync(parsedData); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
## Final Words |
|||
|
|||
Use **Background Workers** for stateful, continuous operations and **Background Jobs** for isolated, retriable units of work. |
|||
|
|||
I hope this article will clarify these terms in ABP Framework. |
|||
|
|||
https://abp.io/ a complete web application platform. |
|||
|
|||
Happy coding😊 |
|||
|
|||
|
|||
|
After Width: | Height: | Size: 648 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 41 KiB |
@ -0,0 +1,77 @@ |
|||
# Fixing OpenIddict Certificate Issues in IIS or Azure |
|||
|
|||
When deploying an ABP application with OpenIddict to IIS or Azure, you may encounter issues with loading PFX/PKCS12 certificates. This article explains how to properly configure certificate loading to ensure it works correctly in these environments. |
|||
|
|||
## The Problem |
|||
|
|||
When running under IIS or Azure, the application pool identity may not have sufficient permissions to access certificate private keys. This commonly results in errors such as: |
|||
|
|||
- `System.Security.Cryptography.CryptographicException: Access denied.` |
|||
- `WindowsCryptographicException: Access is denied.` |
|||
- `System.Security.Cryptography.CryptographicException: The system cannot find the file specified.` |
|||
|
|||
## The Solution |
|||
|
|||
### Using AddDevelopmentEncryptionAndSigningCertificate |
|||
|
|||
For development environments using `DevelopmentEncryptionAndSigningCertificate`, you must configure the application pool to load a user profile. |
|||
|
|||
> Note: We strongly recommend using `DevelopmentEncryptionAndSigningCertificate` only in development environments. For production, always create and use a separate certificate. |
|||
|
|||
 |
|||
|
|||
### Using AddProductionEncryptionAndSigningCertificate |
|||
|
|||
The ABP OpenIddict module provides an `AddProductionEncryptionAndSigningCertificate` extension method. By default, the template project attempts to load an `openiddict.pfx` certificate in production environments. |
|||
|
|||
To ensure proper certificate loading in IIS or Azure, you need to specify appropriate `X509KeyStorageFlags` when calling this method: |
|||
|
|||
```csharp |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var hostingEnvironment = context.Services.GetHostingEnvironment(); |
|||
|
|||
if (!hostingEnvironment.IsDevelopment()) |
|||
{ |
|||
PreConfigure<AbpOpenIddictAspNetCoreOptions>(options => |
|||
{ |
|||
options.AddDevelopmentEncryptionAndSigningCertificate = false; |
|||
}); |
|||
|
|||
PreConfigure<OpenIddictServerBuilder>(serverBuilder => |
|||
{ |
|||
var flag = X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.EphemeralKeySet; |
|||
serverBuilder.AddProductionEncryptionAndSigningCertificate("openiddict.pfx", "YourCertificatePassword", flag); |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Understanding X509KeyStorageFlags |
|||
|
|||
The configuration uses two important flags: |
|||
|
|||
* `X509KeyStorageFlags.MachineKeySet`: Specifies that the key belongs to the local computer key store, binding the key pair's lifecycle to the computer rather than a specific user. |
|||
* `X509KeyStorageFlags.EphemeralKeySet`: Indicates that the key will be stored only in memory and not persisted to disk or key store, enhancing security for runtime-only certificate requirements. |
|||
|
|||
Using these flags in combination helps prevent permission-related issues in IIS and Azure environments. |
|||
|
|||
## Troubleshooting Guide |
|||
|
|||
If you continue to experience issues, verify the following: |
|||
|
|||
* Confirm that the certificate password is correct |
|||
* Verify that the `openiddict.pfx` file exists in your deployment |
|||
* Ensure the certificate is valid - you can generate a new one using: |
|||
```bash |
|||
dotnet dev-certs https -v -ep openiddict.pfx -p YourCertificatePassword |
|||
``` |
|||
* Check the stdout logs for related errors (See [how to get stdout-log](https://abp.io/docs/latest/solution-templates/layered-web-application/deployment/deployment-iis?UI=Blazor&DB=EF&Tiered=No#how-to-get-stdout-log)) |
|||
|
|||
## References |
|||
|
|||
- [ABP OpenIddict Deployment](https://abp.io/docs/latest/solution-templates/layered-web-application/deployment/openiddict-deployment) |
|||
- [ABP IIS Deployment](https://abp.io/docs/latest/solution-templates/layered-web-application/deployment/deployment-iis) |
|||
- [ABP Azure Deployment](https://abp.io/docs/latest/solution-templates/layered-web-application/deployment/azure-deployment/azure-deployment) |
|||
- [How to Generate a New Certificate](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-dev-certs#how-to-generate-a-new-certificate) |
|||
- [Load User Profile in IIS](https://learn.microsoft.com/en-us/iis/manage/configuring-security/application-pool-identities#load-user-profile-for-an-application-pool) |
|||
@ -0,0 +1,325 @@ |
|||
# Understanding Transactions in ABP Unit of Work |
|||
|
|||
[The Unit of Work](https://en.wikipedia.org/wiki/Unit_of_work) is a software design pattern that maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems to ensure that all changes are made within a single transaction. |
|||
|
|||
## Transaction Management Overview |
|||
|
|||
One of the primary responsibilities of the Unit of Work is managing database transactions. It provides the following transaction management features: |
|||
|
|||
- Automatically manages database connections and transaction scopes, eliminating the need for manual transaction control |
|||
- Ensures business operation integrity by making all database operations within a unit of work either succeed or roll back completely |
|||
- Supports configuration of transaction isolation levels and timeout periods |
|||
- Supports nested transactions and transaction propagation |
|||
|
|||
## Transaction Behavior |
|||
|
|||
### Default Transaction Settings |
|||
|
|||
You can modify the default behavior through the following configuration: |
|||
|
|||
```csharp |
|||
Configure<AbpUnitOfWorkDefaultOptions>(options => |
|||
{ |
|||
/* |
|||
Modify the default transaction behavior for all unit of work: |
|||
- UnitOfWorkTransactionBehavior.Enabled: Always enable transactions, all requests will start a transaction |
|||
- UnitOfWorkTransactionBehavior.Disabled: Always disable transactions, no requests will start a transaction |
|||
- UnitOfWorkTransactionBehavior.Auto: Automatically decide whether to start a transaction based on HTTP request type |
|||
*/ |
|||
options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled; |
|||
|
|||
// Set default timeout |
|||
options.Timeout = TimeSpan.FromSeconds(30); |
|||
|
|||
// Set default isolation level |
|||
options.IsolationLevel = IsolationLevel.ReadCommitted; |
|||
}); |
|||
``` |
|||
|
|||
### Automatic Transaction Management |
|||
|
|||
ABP Framework implements automatic management of Unit of Work and transactions through middlewares, MVC global filters, and interceptors. In most cases, you don't need to manage them manually |
|||
|
|||
### Transaction Behavior for HTTP Requests |
|||
|
|||
By default, the framework adopts an intelligent transaction management strategy for HTTP requests: |
|||
- `GET` requests won't start a transactional unit of work because there is no data modification |
|||
- Other HTTP requests (`POST/PUT/DELETE` etc.) will start a transactional unit of work |
|||
|
|||
### Manual Transaction Control |
|||
|
|||
If you need to manually start a new unit of work, you can customize whether to start a transaction and set the transaction isolation level and timeout: |
|||
|
|||
```csharp |
|||
// Start a transactional unit of work |
|||
using (var uow = _unitOfWorkManager.Begin( |
|||
isTransactional: true, |
|||
isolationLevel: IsolationLevel.RepeatableRead, |
|||
timeout: 30 |
|||
)) |
|||
{ |
|||
// Execute database operations within transaction |
|||
await uow.CompleteAsync(); |
|||
} |
|||
``` |
|||
|
|||
```csharp |
|||
// Start a non-transactional unit of work |
|||
using (var uow = _unitOfWorkManager.Begin( |
|||
isTransactional: false |
|||
)) |
|||
{ |
|||
// Execute database operations without transaction |
|||
await uow.CompleteAsync(); |
|||
} |
|||
``` |
|||
|
|||
### Configuring Transactions Using `[UnitOfWork]` Attribute |
|||
|
|||
You can customize transaction behavior by using the `UnitOfWorkAttribute` on methods, classes, or interfaces: |
|||
|
|||
```csharp |
|||
[UnitOfWork( |
|||
IsTransactional = true, |
|||
IsolationLevel = IsolationLevel.RepeatableRead, |
|||
Timeout = 30 |
|||
)] |
|||
public virtual async Task ProcessOrderAsync(int orderId) |
|||
{ |
|||
// Execute database operations within transaction |
|||
} |
|||
``` |
|||
|
|||
### Non-Transactional Unit of Work |
|||
|
|||
In some scenarios, you might not need transaction support. You can create a non-transactional unit of work by setting `IsTransactional = false`: |
|||
|
|||
```csharp |
|||
public virtual async Task ImportDataAsync(List<DataItem> items) |
|||
{ |
|||
using (var uow = _unitOfWorkManager.Begin( |
|||
isTransactional: false |
|||
)) |
|||
{ |
|||
foreach (var item in items) |
|||
{ |
|||
await _repository.InsertAsync(item, autoSave: true); |
|||
// Each InsertAsync will save to database immediately |
|||
// If subsequent operations fail, saved data won't be rolled back |
|||
} |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Applicable scenarios: |
|||
- Batch import data scenarios where partial success is accepted |
|||
- Read-only operations, such as queries |
|||
- Scenarios with low data consistency requirements |
|||
|
|||
### Methods to Commit Transactions |
|||
|
|||
#### In Transactional Unit of Work |
|||
|
|||
A Unit of Work provides several methods to commit changes to the database: |
|||
|
|||
1. **IUnitOfWork.SaveChangesAsync** |
|||
|
|||
```csharp |
|||
await _unitOfWorkManager.Current.SaveChangesAsync(); |
|||
``` |
|||
|
|||
2. **autoSave parameter in repositories** |
|||
|
|||
```csharp |
|||
await _repository.InsertAsync(entity, autoSave: true); |
|||
``` |
|||
|
|||
Both `autoSave` and `SaveChangesAsync` commit changes in the current context to the database. However, these are not applied until `CompleteAsync` is called. If the unit of work throws an exception or `CompleteAsync` is not called, the transaction will be rolled back. It means all the DB operations will be reverted back. Only after successfully executing `CompleteAsync` will the transaction be permanently committed to the database. |
|||
|
|||
3. **CompleteAsync** |
|||
|
|||
```csharp |
|||
using (var uow = _unitOfWorkManager.Begin()) |
|||
{ |
|||
// Execute database operations |
|||
await uow.CompleteAsync(); |
|||
} |
|||
``` |
|||
|
|||
When you manually control the Unit of Work with `UnitOfWorkManager`, the `CompleteAsync` method is crucial for transaction completion. The unit of work maintains a `DbTransaction` object internally, and the `CompleteAsync` method invokes `DbTransaction.CommitAsync` to commit the transaction. The transaction will not be committed if `CompleteAsync` is either not executed or fails to execute successfully. |
|||
|
|||
This method not only commits all database transactions but also: |
|||
|
|||
- Executes and processes all pending domain events within the Unit of Work |
|||
- Executes all registered post-operations and cleanup tasks within the Unit of Work |
|||
- Releases all DbTransaction resources upon disposal of the Unit of Work object |
|||
|
|||
> Note: `CompleteAsync` method should be called only once. Multiple calls are not supported. |
|||
|
|||
#### In Non-Transactional Unit of Work |
|||
|
|||
In non-transactional Unit of Work, these methods behave differently: |
|||
|
|||
Both `autoSave` and `SaveChangesAsync` will persist changes to the database immediately, and these changes cannot be rolled back. Even in non-transactional Unit of Work, calling the `CompleteAsync` method remains necessary as it handles other essential tasks. |
|||
|
|||
Example: |
|||
```csharp |
|||
using (var uow = _unitOfWorkManager.Begin(isTransactional: false)) |
|||
{ |
|||
// Changes are persisted immediately and cannot be rolled back |
|||
await _repository.InsertAsync(entity1, autoSave: true); |
|||
|
|||
// This operation persists independently of the previous operation |
|||
await _repository.InsertAsync(entity2, autoSave: true); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
``` |
|||
|
|||
### Methods to Roll Back Transactions |
|||
|
|||
#### In Transactional Unit of Work |
|||
|
|||
A unit of work provides multiple approaches to roll back transactions: |
|||
|
|||
1. **Automatic Rollback** |
|||
|
|||
For transactions automatically managed by the ABP Framework, any uncaught exceptions during the request will trigger an automatic rollback. |
|||
|
|||
2. **Manual Rollback** |
|||
|
|||
For manually managed transactions, you can explicitly invoke the `RollbackAsync` method to immediately roll back the current transaction. |
|||
|
|||
> Important: Once `RollbackAsync` is called, the entire Unit of Work transaction will be rolled back immediately, and any subsequent calls to `CompleteAsync` will have no effect. |
|||
|
|||
```csharp |
|||
using (var uow = _unitOfWorkManager.Begin( |
|||
isTransactional: true, |
|||
isolationLevel: IsolationLevel.RepeatableRead, |
|||
timeout: 30 |
|||
)) |
|||
{ |
|||
await _repository.InsertAsync(entity); |
|||
|
|||
if (someCondition) |
|||
{ |
|||
await uow.RollbackAsync(); |
|||
return; |
|||
} |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
``` |
|||
|
|||
The `CompleteAsync` method attempts to commit the transaction. If any exceptions occur during this process, the transaction will not be committed. |
|||
|
|||
Here are two common exception scenarios: |
|||
|
|||
1. **Exception Handling Within Unit of Work** |
|||
|
|||
```csharp |
|||
using (var uow = _unitOfWorkManager.Begin( |
|||
isTransactional: true, |
|||
isolationLevel: IsolationLevel.RepeatableRead, |
|||
timeout: 30 |
|||
)) |
|||
{ |
|||
try |
|||
{ |
|||
await _bookRepository.InsertAsync(book); |
|||
await uow.SaveChangesAsync(); |
|||
await _productRepository.UpdateAsync(product); |
|||
await uow.CompleteAsync(); |
|||
} |
|||
catch (Exception) |
|||
{ |
|||
// Exceptions can occur in InsertAsync, SaveChangesAsync, UpdateAsync, or CompleteAsync |
|||
// Even if some operations succeed, the transaction remains uncommitted to the database |
|||
// While you can explicitly call RollbackAsync to roll back the transaction, |
|||
// the transaction will not be committed anyway if CompleteAsync fails to execute |
|||
throw; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
2. **Exception Handling Outside Unit of Work** |
|||
|
|||
```csharp |
|||
try |
|||
{ |
|||
using (var uow = _unitOfWorkManager.Begin( |
|||
isTransactional: true, |
|||
isolationLevel: IsolationLevel.RepeatableRead, |
|||
timeout: 30 |
|||
)) |
|||
{ |
|||
await _bookRepository.InsertAsync(book); |
|||
await uow.SaveChangesAsync(); |
|||
await _productRepository.UpdateAsync(product); |
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
catch (Exception) |
|||
{ |
|||
// Exceptions can occur in UpdateAsync, SaveChangesAsync, UpdateAsync, or CompleteAsync |
|||
// Even if some operations succeed, the transaction remains uncommitted to the database |
|||
// Since CompleteAsync was not successfully executed, the transaction will not be committed |
|||
throw; |
|||
} |
|||
``` |
|||
|
|||
#### In Non-Transactional Unit of Work |
|||
|
|||
In non-transactional units of work, operations are irreversible. Changes saved using `autoSave: true` or `SaveChangesAsync()` are persisted immediately, and the `RollbackAsync` method has no effect. |
|||
|
|||
## Transaction Management Best Practices |
|||
|
|||
### 1. Remember to Commit Transactions |
|||
|
|||
When manually controlling transactions, remember to call the `CompleteAsync` method to commit the transaction after operations are complete. |
|||
|
|||
### 2. Pay Attention to Context |
|||
|
|||
If a unit of work already exists in the current context, `UnitOfWorkManager.Begin` method and` UnitOfWorkAttribute` will **reuse it**. Specify `requiresNew: true` to force create a new unit of work. |
|||
|
|||
```csharp |
|||
[UnitOfWork] |
|||
public async Task Method1() |
|||
{ |
|||
using (var uow = _unitOfWorkManager.Begin( |
|||
requiresNew: true, |
|||
isTransactional: true, |
|||
isolationLevel: IsolationLevel.RepeatableRead, |
|||
timeout: 30 |
|||
)) |
|||
{ |
|||
await Method2(); |
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 3. Use `virtual` Methods |
|||
|
|||
To be able to use Unit of Work attribute, you must use the `virtual` modifier for methods in dependency injection class services, because ABP Framework uses interceptors, and it cannot intercept non `virtual` methods, thus unable to implement Unit of Work functionality. |
|||
|
|||
### 4. Avoid Long Transactions |
|||
|
|||
Enabling long-running transactions can lead to resource locking, excessive transaction log usage, and reduced concurrent performance, while rollback costs are high and may exhaust database connection resources. It's recommended to split into shorter transactions, reduce lock holding time, and optimize performance and reliability. |
|||
|
|||
## Transaction-Related Recommendations |
|||
|
|||
- Choose appropriate transaction isolation levels based on business requirements |
|||
- Avoid overly long transactions, long-running operations should be split into multiple small transactions |
|||
- Use the `requiresNew` parameter reasonably to control transaction boundaries |
|||
- Pay attention to setting appropriate transaction timeout periods |
|||
- Ensure transactions can properly roll back when exceptions occur |
|||
- For read-only operations, it's recommended to use non-transactional Unit of Work to improve performance |
|||
|
|||
## References |
|||
|
|||
- [ABP Unit of Work](https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work) |
|||
- [EF Core Transactions](https://docs.microsoft.com/en-us/ef/core/saving/transactions) |
|||
- [Transaction Isolation Levels](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel) |
|||
|
After Width: | Height: | Size: 1.1 MiB |
@ -0,0 +1,114 @@ |
|||
# Customizing Authentication Flow with OpenIddict Events in ABP Framework |
|||
|
|||
[ABP's OpenIddict Module](https://abp.io/docs/latest/modules/openiddict) provides an integration with the [OpenIddict](https://github.com/openiddict/openiddict-core) library, which provides advanced authentication features like **single sign-on**, **single log-out**, and **API access control**. |
|||
|
|||
OpenIddict provides an event-driven model ([event models](https://documentation.openiddict.com/introduction#events-model)) that allows developers to customize authentication and authorization processes. This event model enables handling actions such as user **sign-in**, **sign-out**, **token validation**, and **request handling** dynamically. |
|||
|
|||
In this article, we will explore OpenIddict event models, their key use cases, and how to implement them effectively. |
|||
|
|||
## Understanding OpenIddict Event Model |
|||
|
|||
OpenIddict events are primarily used within the OpenIddict server component. These events provide hooks into the OpenID Connect flow, allowing developers to modify behavior at different stages of authentication & authorization processes. |
|||
|
|||
They are triggered during critical moments such as: |
|||
|
|||
* User authentication (sign-in) |
|||
* Session termination (sign-out) |
|||
* Token validation and generation |
|||
* Request processing |
|||
* Error handling |
|||
|
|||
OpenIddict provides multiple server events, under the `OpenIddictServerEvents` static class to make them easier to find (also provides additonal validation events under the `OpenIddictValidationEvents` static class). |
|||
|
|||
Here are some of the pre-defined `OpenIddictServerEvents`: |
|||
|
|||
 |
|||
|
|||
Each event represents a specific checkpoint in the **request processing pipeline**, such as validating an OpenID Connect request, extracting request parameters, processing the request, or generating a response. As an application developer, you simply need to create event handlers that subscribe to these predefined events to implement your custom logic at the desired pipeline stage. |
|||
|
|||
## Example: How to add custom logic when a user signs out? |
|||
|
|||
Let's walkthrough a practical example of implementing custom sign-out logic using OpenIddict events. |
|||
|
|||
### Step 1: Create a Custom Event Handler |
|||
|
|||
First, create a handler that implements `IOpenIddictServerHandler<OpenIddictServerEvents.ProcessSignOutContext>`: |
|||
|
|||
```csharp |
|||
using System.Threading.Tasks; |
|||
using OpenIddict.Server; |
|||
|
|||
namespace MySolution; |
|||
|
|||
public class SignOutEventHandler : IOpenIddictServerHandler<OpenIddictServerEvents.ProcessSignOutContext> |
|||
{ |
|||
public static OpenIddictServerHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictServerHandlerDescriptor.CreateBuilder<OpenIddictServerEvents.ProcessSignOutContext>() |
|||
.UseSingletonHandler<SignOutEventHandler>() |
|||
.SetOrder(100_000) |
|||
.SetType(OpenIddictServerHandlerType.Custom) |
|||
.Build(); |
|||
|
|||
public ValueTask HandleAsync(OpenIddictServerEvents.ProcessSignOutContext context) |
|||
{ |
|||
// Implement your custom sign-out logic here |
|||
|
|||
// Examples: |
|||
// - Clear custom session data |
|||
// - Perform audit logging |
|||
// - Notify other services |
|||
// - Clean up user-specific resources |
|||
|
|||
return ValueTask.CompletedTask; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
The handler configuration includes several important components: |
|||
|
|||
* `Descriptor` - Defines how the handler should be registered and executed |
|||
* `SetOrder` - Determines the execution order when multiple handlers exist |
|||
* `SetType` - Specifies this as a custom handler implementation |
|||
* `UseSingletonHandler` - Sets lifetime of the class as _Singleton_ |
|||
|
|||
### Step 2: Register the Event Handler |
|||
|
|||
Register your custom handler in your application's module configuration: |
|||
|
|||
```csharp |
|||
//... |
|||
|
|||
public class MySolutionAuthServerModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
PreConfigure<OpenIddictServerBuilder>(serverBuilder => |
|||
{ |
|||
serverBuilder.AddEventHandler(SignOutEventHandler.Descriptor); |
|||
}); |
|||
} |
|||
|
|||
//... |
|||
} |
|||
``` |
|||
|
|||
That's it! After these steps, your `SignOutEventHandler.HandleAsync()` method should be triggered after each signout request. You can also use other pre-defined server events for other stages of the authentication & authorization processes such as; |
|||
|
|||
* `OpenIddictServerEvents.ProcessSignInContext` -> after each sign-in, |
|||
* `OpenIddictServerEvents.ProcessErrorContext` -> when an error occurs in the authentication, |
|||
* `OpenIddictServerEvents.ProcessChallengeContext` -> called when processing a challenge operation, |
|||
* and other 40+ server events... |
|||
|
|||
Each event provides access to the relevant context, allowing you to access and modify the authentication flow's behavior. |
|||
|
|||
## Conclusion |
|||
|
|||
ABP Framework integrates OpenIddict as its authentication and authorization module. OpenIddict provides an event-driven model that allows developers to customize authentication and authorization processes within their ABP applications. It's pre-installed & pre-configured in the ABP's startup templates. |
|||
|
|||
OpenIddict provides a powerful and flexible way to customize authentication flows. By leveraging these events, developers can implement complex authentication scenarios while maintaining clean, maintainable code. |
|||
|
|||
## References |
|||
|
|||
* [OpenIddict Documentation](https://documentation.openiddict.com/introduction#events-model) |
|||
* [ABP OpenIddict Module Documentation](https://abp.io/docs/latest/modules/openiddict) |
|||
* [Advanced OpenIddict Scenarios](https://kevinchalet.com/2018/07/02/implementing-advanced-scenarios-using-the-new-openiddict-rc3-events-model/) |
|||
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 47 KiB |
@ -0,0 +1,64 @@ |
|||
# BLOB Storing Bunny Provider |
|||
|
|||
BLOB Storing Bunny Provider can store BLOBs in [bunny.net Storage](https://bunny.net/storage/). |
|||
|
|||
> Read the [BLOB Storing document](../blob-storing) to understand how to use the BLOB storing system. This document only covers how to configure containers to use a Bunny BLOB as the storage provider. |
|||
|
|||
## Installation |
|||
|
|||
Use the ABP CLI to add [Volo.Abp.BlobStoring.Bunny](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Bunny) NuGet package to your project: |
|||
|
|||
* Install the [ABP CLI](../../../cli) if you haven't installed before. |
|||
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BlobStoring.Bunny` package. |
|||
* Run `abp add-package Volo.Abp.BlobStoring.Bunny` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.BlobStoring.Bunny](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Bunny) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringBunnyModule))]` to the [ABP module](../../architecture/modularity/basics.md) class inside your project. |
|||
|
|||
## Configuration |
|||
|
|||
Configuration is done in the `ConfigureServices` method of your [module](../../architecture/modularity/basics.md) class, as explained in the [BLOB Storing document](../blob-storing). |
|||
|
|||
**Example: Configure to use the Bunny storage provider by default** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseBunny(Bunny => |
|||
{ |
|||
Bunny.AccessKey = "your Bunny account access key"; |
|||
Bunny.Region = "the code of the main storage zone region"; // "de" is the default value |
|||
Bunny.ContainerName = "your bunny storage zone name"; |
|||
Bunny.CreateContainerIfNotExists = true; |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
```` |
|||
|
|||
> See the [BLOB Storing document](../blob-storing) to learn how to configure this provider for a specific container. |
|||
|
|||
### Options |
|||
|
|||
* **AccessKey** (string): Bunny Account Access Key. [Where do I find my Access key?](https://support.bunny.net/hc/en-us/articles/360012168840-Where-do-I-find-my-API-key) |
|||
* **Region** (string?): The code of the main storage zone region (Possible values: DE, NY, LA, SG). |
|||
* **ContainerName** (string): You can specify the container name in Bunny. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](../blob-storing)). Please note that Bunny has some **rules for naming containers**: |
|||
* Storage Zone names must be a globaly unique. |
|||
* Storage Zone names must be between **4** and **64** characters long. |
|||
* Storage Zone names can consist only of **lowercase** letters, numbers, and hyphens (-). |
|||
* **CreateContainerIfNotExists** (bool): Default value is `false`, If a container does not exist in Bunny, `BunnyBlobProvider` will try to create it. |
|||
|
|||
## Bunny Blob Name Calculator |
|||
|
|||
Bunny Blob Provider organizes BLOB name and implements some conventions. The full name of a BLOB is determined by the following rules by default: |
|||
|
|||
* Appends `host` string if [current tenant](../../architecture/multi-tenancy) is `null` (or multi-tenancy is disabled for the container - see the [BLOB Storing document](../blob-storing) to learn how to disable multi-tenancy for a container). |
|||
* Appends `tenants/<tenant-id>` string if current tenant is not `null`. |
|||
* Appends the BLOB name. |
|||
|
|||
## Other Services |
|||
|
|||
* `BunnyBlobProvider` is the main service that implements the Bunny BLOB storage provider, if you want to override/replace it via [dependency injection](../../fundamentals/dependency-injection.md) (don't replace `IBlobProvider` interface, but replace `BunnyBlobProvider` class). |
|||
* `IBunnyBlobNameCalculator` is used to calculate the full BLOB name (that is explained above). It is implemented by the `DefaultBunnyBlobNameCalculator` by default. |
|||
* `IBunnyClientFactory` is implemented by `DefaultBunnyClientFactory` by default. You can override/replace it,if you want customize. |
|||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 41 KiB |
@ -0,0 +1,59 @@ |
|||
# Migrating from MongoDB Driver 2 to 3 |
|||
|
|||
## Introduction |
|||
|
|||
The release of MongoDB Driver 3 includes numerous user-requested fixes and improvements that were deferred in previous versions due to backward compatibility concerns. It also features internal improvements to reduce technical debt and enhance maintainability. One major update is the removal of a significant portion of the public API (primarily from `MongoDB.Driver.Core`), which was not intended for public use. The removed APIs were marked as deprecated in version 2.30.0. |
|||
|
|||
Please refer to the [upgrade guide](https://www.mongodb.com/docs/drivers/csharp/current/upgrade/v3/) for a complete list of breaking changes and upgrade guidelines. |
|||
|
|||
## Repository Changes |
|||
|
|||
Some method signatures in the `MongoDbRepository` class have been updated because the `IMongoQueryable` has been removed. The specific changes are as follows: |
|||
|
|||
- The new `GetQueryableAsync` method has been added to return `IQueryable<TEntity>`. |
|||
- The `GetMongoQueryable` and `GetMongoQueryableAsync` methods return `IQueryable<TEntity>` instead of `IMongoQueryable<TEntity>`, |
|||
- The `GetMongoQueryable` and `GetMongoQueryableAsync` methods are marked as obsolete, You should use the new `GetQueryableAsync` method instead. |
|||
|
|||
Please update your application by searching for and replacing these method calls. |
|||
|
|||
> The return value of the `GetQueryableAsync` method is `IQueryable<TEntity>`, which can be used directly to perform queries, similar to EF Core. Remove all instances of `IMongoQueryable` in your project and replace them with `IQueryable`. |
|||
|
|||
**Previous code example:** |
|||
|
|||
```csharp |
|||
var myEntity = await (await GetMongoQueryableAsync()).As<IMongoQueryable<MyEntity>>().FirstOrDefaultAsync(x => x.Id == id); |
|||
``` |
|||
|
|||
**Updated code example:** |
|||
|
|||
```csharp |
|||
var myEntity = await GetQueryableAsync().FirstOrDefaultAsync(x => x.Id == id); |
|||
``` |
|||
|
|||
## Unit Test Changes |
|||
|
|||
Previously, we used the [EphemeralMongo](https://github.com/asimmon/ephemeral-mongo) library for unit testing. However, it does not support the latest version of [MongoDB.Driver 3.x](https://github.com/mongodb/mongo-go-driver). You should replace it with [MongoSandbox](https://github.com/wassim-k/MongoSandbox). |
|||
|
|||
In your unit test project files, replace the following: |
|||
|
|||
```xml |
|||
<PackageReference Include="EphemeralMongo.Core" Version="1.1.3" /> |
|||
<PackageReference Include="EphemeralMongo6.runtime.linux-x64" Version="1.1.3" Condition="$([MSBuild]::IsOSPlatform('Linux'))" /> |
|||
<PackageReference Include="EphemeralMongo6.runtime.osx-x64" Version="1.1.3" Condition="$([MSBuild]::IsOSPlatform('OSX'))" /> |
|||
<PackageReference Include="EphemeralMongo6.runtime.win-x64" Version="1.1.3" Condition="$([MSBuild]::IsOSPlatform('Windows'))" /> |
|||
``` |
|||
|
|||
With: |
|||
|
|||
```xml |
|||
<PackageReference Include="MongoSandbox.Core" Version="1.0.1" /> |
|||
<PackageReference Include="MongoSandbox6.runtime.linux-x64" Version="1.0.1" Condition="$([MSBuild]::IsOSPlatform('Linux'))" /> |
|||
<PackageReference Include="MongoSandbox6.runtime.osx-x64" Version="1.0.1" Condition="$([MSBuild]::IsOSPlatform('OSX'))" /> |
|||
<PackageReference Include="MongoSandbox6.runtime.win-x64" Version="1.0.1" Condition="$([MSBuild]::IsOSPlatform('Windows'))" /> |
|||
``` |
|||
|
|||
In your unit test classes, replace `using EphemeralMongo` with `using MongoSandbox`. |
|||
|
|||
## Official Upgrade Guide |
|||
|
|||
We recommend reviewing the [upgrade guide](https://www.mongodb.com/docs/drivers/csharp/current/upgrade/v3/) for MongoDB Driver 3 to ensure a smooth migration process. |
|||
@ -1,15 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Toastr; |
|||
|
|||
[DependsOn(typeof(JQueryScriptContributor))] |
|||
public class ToastrScriptBundleContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.AddIfNotContains("/libs/toastr/toastr.min.js"); |
|||
} |
|||
} |
|||
@ -1,12 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Toastr; |
|||
|
|||
public class ToastrStyleBundleContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.AddIfNotContains("/libs/toastr/toastr.min.css"); |
|||
} |
|||
} |
|||
@ -0,0 +1,139 @@ |
|||
.abp-toast-container { |
|||
position: fixed; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: flex-end; |
|||
min-width: 350px; |
|||
min-height: 80px; |
|||
z-index: 1900; |
|||
right: 30px; |
|||
bottom: 30px; |
|||
} |
|||
|
|||
.abp-toast { |
|||
display: grid; |
|||
grid-template-columns: 35px 1fr; |
|||
gap: 5px; |
|||
margin: 5px 0; |
|||
padding: 10px; |
|||
width: 350px; |
|||
user-select: none; |
|||
z-index: 9999; |
|||
color: #fff; |
|||
border-radius: 8px; |
|||
font-size: 16px; |
|||
box-shadow: 0 0 20px 0 rgba(76, 87, 125, 0.02); |
|||
animation: toastIn 0.3s ease-in-out; |
|||
} |
|||
|
|||
.abp-toast-success { |
|||
border: 2px solid #4fbf67; |
|||
background-color: #4fbf67; |
|||
} |
|||
|
|||
.abp-toast-error { |
|||
border: 2px solid #c00d49; |
|||
background-color: #c00d49; |
|||
} |
|||
|
|||
.abp-toast-info { |
|||
border: 2px solid #438aa7; |
|||
background-color: #438aa7; |
|||
} |
|||
|
|||
.abp-toast-warning { |
|||
border: 2px solid #ff9f38; |
|||
background-color: #ff9f38; |
|||
} |
|||
|
|||
.abp-toast-icon { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.abp-toast-icon .icon { |
|||
font-size: 30px; |
|||
} |
|||
|
|||
.abp-toast-content { |
|||
position: relative; |
|||
display: flex; |
|||
align-self: center; |
|||
flex-direction: column; |
|||
word-break: break-word; |
|||
padding-bottom: 2px; |
|||
} |
|||
|
|||
.abp-toast-close-button { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
margin: 0; |
|||
padding: 0px 5px 0 0; |
|||
width: 25px; |
|||
height: 100%; |
|||
border: none; |
|||
border-radius: 50%; |
|||
background: transparent; |
|||
color: inherit; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.abp-toast-close-button:focus { |
|||
outline: none; |
|||
} |
|||
|
|||
.abp-toast-title { |
|||
margin: 0; |
|||
padding: 0; |
|||
font-size: 1rem; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
.abp-toast-message { |
|||
margin: 0; |
|||
padding: 0; |
|||
max-width: 240px; |
|||
} |
|||
|
|||
@keyframes toastIn { |
|||
from { |
|||
transform: translateX(100%); |
|||
opacity: 0; |
|||
} |
|||
to { |
|||
transform: translateX(0); |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
@keyframes toastOut { |
|||
from { |
|||
transform: translateX(0); |
|||
opacity: 1; |
|||
} |
|||
to { |
|||
transform: translateX(100%); |
|||
opacity: 0; |
|||
} |
|||
} |
|||
|
|||
.toast-removing { |
|||
animation: toastOut 0.3s ease-in-out forwards; |
|||
} |
|||
|
|||
@media only screen and (max-width: 768px) { |
|||
.abp-toast-container { |
|||
min-width: 100%; |
|||
right: 0; |
|||
} |
|||
|
|||
.abp-toast { |
|||
width: 95%; |
|||
} |
|||
} |
|||
@ -0,0 +1,207 @@ |
|||
function AbpToastService(globalOptions) { |
|||
// Static default configuration for all instances
|
|||
if (!AbpToastService.defaultOptions) { |
|||
AbpToastService.defaultOptions = { |
|||
closable: true, |
|||
sticky: false, |
|||
life: 5000, |
|||
tapToDismiss: false, |
|||
containerKey: undefined, |
|||
iconClass: undefined, |
|||
position: { |
|||
top: 'auto', |
|||
right: '30px', |
|||
bottom: '30px', |
|||
left: 'auto' |
|||
} |
|||
}; |
|||
} |
|||
|
|||
// Find existing container or create new one
|
|||
const containerId = globalOptions?.containerKey ? |
|||
`toast-container-${globalOptions.containerKey}` : |
|||
'toast-container'; |
|||
|
|||
this.container = document.getElementById(containerId); |
|||
if (!this.container) { |
|||
this.container = document.createElement('div'); |
|||
this.container.id = containerId; |
|||
this.container.className = 'abp-toast-container'; |
|||
document.body.appendChild(this.container); |
|||
} |
|||
|
|||
this.toasts = []; |
|||
this.lastId = 0; |
|||
|
|||
// Merge user provided global options with defaults
|
|||
this.globalOptions = this.extend({}, AbpToastService.defaultOptions, globalOptions); |
|||
|
|||
this.updateContainerPosition(); |
|||
} |
|||
|
|||
// Deep merge objects
|
|||
AbpToastService.prototype.extend = function(target, ...sources) { |
|||
sources.forEach(source => { |
|||
for (const key in source) { |
|||
if (source[key] && typeof source[key] === 'object') { |
|||
target[key] = this.extend(target[key] || {}, source[key]); |
|||
} else { |
|||
target[key] = source[key]; |
|||
} |
|||
} |
|||
}); |
|||
return target; |
|||
}; |
|||
|
|||
// Update toast container position based on global options
|
|||
AbpToastService.prototype.updateContainerPosition = function() { |
|||
const { position } = this.globalOptions; |
|||
Object.assign(this.container.style, position); |
|||
}; |
|||
|
|||
// Update global options
|
|||
AbpToastService.prototype.setGlobalOptions = function(options) { |
|||
this.globalOptions = this.extend({}, this.globalOptions, options); |
|||
this.updateContainerPosition(); |
|||
}; |
|||
|
|||
// Get icon class based on severity
|
|||
AbpToastService.prototype.getIconClass = function(severity, options) { |
|||
// Use custom icon class if provided
|
|||
if (options.iconClass) { |
|||
return options.iconClass; |
|||
} |
|||
|
|||
const icons = { |
|||
success: 'fa fa-check', |
|||
info: 'fa fa-info-circle', |
|||
warning: 'fa fa-exclamation-triangle', |
|||
error: 'fa fa-exclamation-circle' |
|||
}; |
|||
return icons[severity] || 'fa fa-exclamation-triangle'; |
|||
}; |
|||
|
|||
// Create toast DOM element
|
|||
AbpToastService.prototype.createToastElement = function(message, title, severity, options) { |
|||
const toast = document.createElement('div'); |
|||
toast.className = `abp-toast abp-toast-${severity}`; |
|||
|
|||
const closeButton = options.closable !== false ? |
|||
`<button class="abp-toast-close-button">
|
|||
<i class="fa fa-times" aria-hidden="true"></i> |
|||
</button>` : ''; |
|||
|
|||
const titleHtml = title ? `<div class="abp-toast-title">${title}</div>` : ''; |
|||
|
|||
toast.innerHTML = ` |
|||
<div class="abp-toast-icon"> |
|||
<i class="${this.getIconClass(severity, options)} icon" aria-hidden="true"></i> |
|||
</div> |
|||
<div class="abp-toast-content"> |
|||
${closeButton} |
|||
${titleHtml} |
|||
<p class="abp-toast-message">${message}</p> |
|||
</div>`; |
|||
|
|||
// Add event listeners
|
|||
const closeButtonElement = toast.querySelector('.abp-toast-close-button'); |
|||
if (closeButtonElement) { |
|||
closeButtonElement.addEventListener('click', () => this.remove(toast)); |
|||
} |
|||
|
|||
if (options.tapToDismiss) { |
|||
toast.addEventListener('click', () => this.remove(toast)); |
|||
} |
|||
|
|||
return toast; |
|||
}; |
|||
|
|||
// Show a toast with given options
|
|||
AbpToastService.prototype.show = function(message, title, severity = 'neutral', options = {}) { |
|||
const mergedOptions = this.extend({}, this.globalOptions, options); |
|||
const id = ++this.lastId; |
|||
const toast = this.createToastElement(message, title, severity, mergedOptions); |
|||
|
|||
// Set data attributes for non-object options
|
|||
Object.entries(mergedOptions) |
|||
.filter(([_, value]) => typeof value !== 'object') |
|||
.forEach(([key, value]) => toast.dataset[key] = value); |
|||
|
|||
toast.dataset.id = id; |
|||
this.container.appendChild(toast); |
|||
this.toasts.push(toast); |
|||
|
|||
// Auto remove if not sticky
|
|||
if (!mergedOptions.sticky) { |
|||
setTimeout(() => this.remove(toast), mergedOptions.life); |
|||
} |
|||
|
|||
return id; |
|||
}; |
|||
|
|||
// Remove a toast with animation
|
|||
AbpToastService.prototype.remove = function(toastElement) { |
|||
toastElement.classList.add('toast-removing'); |
|||
setTimeout(() => { |
|||
if (toastElement.parentNode === this.container) { |
|||
this.container.removeChild(toastElement); |
|||
} |
|||
this.toasts = this.toasts.filter(t => t !== toastElement); |
|||
}, 300); // Match animation duration
|
|||
}; |
|||
|
|||
// Convenience methods for different severities
|
|||
AbpToastService.prototype.success = function(message, title, options) { |
|||
return this.show(message, title, 'success', options); |
|||
}; |
|||
|
|||
AbpToastService.prototype.error = function(message, title, options) { |
|||
return this.show(message, title, 'error', options); |
|||
}; |
|||
|
|||
AbpToastService.prototype.info = function(message, title, options) { |
|||
return this.show(message, title, 'info', options); |
|||
}; |
|||
|
|||
AbpToastService.prototype.warning = function(message, title, options) { |
|||
return this.show(message, title, 'warning', options); |
|||
}; |
|||
|
|||
// Clear all toasts
|
|||
AbpToastService.prototype.clear = function(containerKey) { |
|||
if (containerKey) { |
|||
this.toasts = this.toasts.filter(toast => { |
|||
const shouldRemove = toast.dataset.containerKey === containerKey; |
|||
if (shouldRemove) { |
|||
this.remove(toast); |
|||
} |
|||
return !shouldRemove; |
|||
}); |
|||
} else { |
|||
this.toasts.forEach(toast => this.remove(toast)); |
|||
} |
|||
}; |
|||
|
|||
// Static method to set default options for all instances
|
|||
AbpToastService.setDefaultOptions = function(options) { |
|||
AbpToastService.defaultOptions = this.prototype.extend({}, AbpToastService.defaultOptions, options); |
|||
}; |
|||
|
|||
var abp = abp || {}; |
|||
(function () { |
|||
abp.notify.success = function (message, title, options) { |
|||
new AbpToastService().success(message, title, options); |
|||
}; |
|||
|
|||
abp.notify.info = function (message, title, options) { |
|||
new AbpToastService().info(message, title, options); |
|||
}; |
|||
|
|||
abp.notify.warn = function (message, title, options) { |
|||
new AbpToastService().warning(message, title, options); |
|||
}; |
|||
|
|||
abp.notify.error = function (message, title, options) { |
|||
new AbpToastService().error(message, title, options); |
|||
}; |
|||
})(); |
|||
@ -1,34 +0,0 @@ |
|||
var abp = abp || {}; |
|||
(function () { |
|||
|
|||
if (!toastr) { |
|||
return; |
|||
} |
|||
|
|||
/* DEFAULTS *************************************************/ |
|||
|
|||
toastr.options.positionClass = 'toast-bottom-right'; |
|||
|
|||
/* NOTIFICATION *********************************************/ |
|||
|
|||
var showNotification = function (type, message, title, options) { |
|||
toastr[type](message, title, options); |
|||
}; |
|||
|
|||
abp.notify.success = function (message, title, options) { |
|||
showNotification('success', message, title, options); |
|||
}; |
|||
|
|||
abp.notify.info = function (message, title, options) { |
|||
showNotification('info', message, title, options); |
|||
}; |
|||
|
|||
abp.notify.warn = function (message, title, options) { |
|||
showNotification('warning', message, title, options); |
|||
}; |
|||
|
|||
abp.notify.error = function (message, title, options) { |
|||
showNotification('error', message, title, options); |
|||
}; |
|||
|
|||
})(); |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"role": "lib.framework" |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
{ |
|||
"name": "Volo.Abp.BlobStoring.Bunny", |
|||
"hash": "", |
|||
"contents": [ |
|||
{ |
|||
"namespace": "Volo.Abp.BlobStoring.Bunny", |
|||
"dependsOnModules": [ |
|||
{ |
|||
"declaringAssemblyName": "Volo.Abp.BlobStoring", |
|||
"namespace": "Volo.Abp.BlobStoring", |
|||
"name": "AbpBlobStoringModule" |
|||
}, |
|||
{ |
|||
"declaringAssemblyName": "Volo.Abp.Caching", |
|||
"namespace": "Volo.Abp.Caching", |
|||
"name": "AbpCachingModule" |
|||
} |
|||
], |
|||
"implementingInterfaces": [ |
|||
{ |
|||
"name": "IAbpModule", |
|||
"namespace": "Volo.Abp.Modularity", |
|||
"declaringAssemblyName": "Volo.Abp.Core", |
|||
"fullName": "Volo.Abp.Modularity.IAbpModule" |
|||
}, |
|||
{ |
|||
"name": "IOnPreApplicationInitialization", |
|||
"namespace": "Volo.Abp.Modularity", |
|||
"declaringAssemblyName": "Volo.Abp.Core", |
|||
"fullName": "Volo.Abp.Modularity.IOnPreApplicationInitialization" |
|||
}, |
|||
{ |
|||
"name": "IOnApplicationInitialization", |
|||
"namespace": "Volo.Abp", |
|||
"declaringAssemblyName": "Volo.Abp.Core", |
|||
"fullName": "Volo.Abp.IOnApplicationInitialization" |
|||
}, |
|||
{ |
|||
"name": "IOnPostApplicationInitialization", |
|||
"namespace": "Volo.Abp.Modularity", |
|||
"declaringAssemblyName": "Volo.Abp.Core", |
|||
"fullName": "Volo.Abp.Modularity.IOnPostApplicationInitialization" |
|||
}, |
|||
{ |
|||
"name": "IOnApplicationShutdown", |
|||
"namespace": "Volo.Abp", |
|||
"declaringAssemblyName": "Volo.Abp.Core", |
|||
"fullName": "Volo.Abp.IOnApplicationShutdown" |
|||
}, |
|||
{ |
|||
"name": "IPreConfigureServices", |
|||
"namespace": "Volo.Abp.Modularity", |
|||
"declaringAssemblyName": "Volo.Abp.Core", |
|||
"fullName": "Volo.Abp.Modularity.IPreConfigureServices" |
|||
}, |
|||
{ |
|||
"name": "IPostConfigureServices", |
|||
"namespace": "Volo.Abp.Modularity", |
|||
"declaringAssemblyName": "Volo.Abp.Core", |
|||
"fullName": "Volo.Abp.Modularity.IPostConfigureServices" |
|||
} |
|||
], |
|||
"contentType": "abpModule", |
|||
"name": "AbpBlobStoringBunnyModule", |
|||
"summary": null |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks> |
|||
<Nullable>enable</Nullable> |
|||
<WarningsAsErrors>Nullable</WarningsAsErrors> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Volo.Abp.BlobStoring\Volo.Abp.BlobStoring.csproj" /> |
|||
<ProjectReference Include="..\Volo.Abp.Caching\Volo.Abp.Caching.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="BunnyCDN.Net.Storage" /> |
|||
<PackageReference Include="Microsoft.Extensions.Http" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,16 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.BlobStoring.Bunny; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpBlobStoringModule), |
|||
typeof(AbpCachingModule))] |
|||
public class AbpBlobStoringBunnyModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddHttpClient(); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.BlobStoring.Bunny; |
|||
|
|||
public class BunnyApiException : Exception |
|||
{ |
|||
public BunnyApiException(string message) |
|||
: base(message) |
|||
{ |
|||
|
|||
} |
|||
|
|||
public BunnyApiException(string message, Exception innerException) |
|||
: base(message, innerException) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.BlobStoring.Bunny; |
|||
|
|||
public static class BunnyBlobContainerConfigurationExtensions |
|||
{ |
|||
public static BunnyBlobProviderConfiguration GetBunnyConfiguration( |
|||
this BlobContainerConfiguration containerConfiguration) |
|||
{ |
|||
return new BunnyBlobProviderConfiguration(containerConfiguration); |
|||
} |
|||
|
|||
public static BlobContainerConfiguration UseBunny( |
|||
this BlobContainerConfiguration containerConfiguration, |
|||
Action<BunnyBlobProviderConfiguration> bunnyConfigureAction) |
|||
{ |
|||
containerConfiguration.ProviderType = typeof(BunnyBlobProvider); |
|||
containerConfiguration.NamingNormalizers.TryAdd<BunnyBlobNamingNormalizer>(); |
|||
|
|||
bunnyConfigureAction(new BunnyBlobProviderConfiguration(containerConfiguration)); |
|||
|
|||
return containerConfiguration; |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
using System.Globalization; |
|||
using System.Text.RegularExpressions; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace Volo.Abp.BlobStoring.Bunny; |
|||
|
|||
public class BunnyBlobNamingNormalizer : IBlobNamingNormalizer, ITransientDependency |
|||
{ |
|||
private readonly static Regex ValidCharactersRegex = |
|||
new Regex(@"^[a-z0-9-]*$", RegexOptions.Compiled); |
|||
|
|||
private const int MinLength = 4; |
|||
private const int MaxLength = 64; |
|||
|
|||
public virtual string NormalizeBlobName(string blobName) => blobName; |
|||
|
|||
public virtual string NormalizeContainerName(string containerName) |
|||
{ |
|||
Check.NotNullOrWhiteSpace(containerName, nameof(containerName)); |
|||
|
|||
using (CultureHelper.Use(CultureInfo.InvariantCulture)) |
|||
{ |
|||
// Trim whitespace and convert to lowercase
|
|||
var normalizedName = containerName |
|||
.Trim() |
|||
.ToLowerInvariant(); |
|||
|
|||
// Remove any invalid characters
|
|||
normalizedName = Regex.Replace(normalizedName, "[^a-z0-9-]", string.Empty); |
|||
|
|||
// Validate structure
|
|||
if (!ValidCharactersRegex.IsMatch(normalizedName)) |
|||
{ |
|||
throw new AbpException( |
|||
$"Container name contains invalid characters: {containerName}. " + |
|||
"Only lowercase letters, numbers, and hyphens are allowed."); |
|||
} |
|||
|
|||
// Validate length
|
|||
if (normalizedName.Length < MinLength || normalizedName.Length > MaxLength) |
|||
{ |
|||
throw new AbpException( |
|||
$"Container name must be between {MinLength} and {MaxLength} characters. " + |
|||
$"Current length: {normalizedName.Length}"); |
|||
} |
|||
|
|||
return normalizedName; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,167 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Net; |
|||
using System.Threading.Tasks; |
|||
using BunnyCDN.Net.Storage; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.BlobStoring.Bunny; |
|||
|
|||
public class BunnyBlobProvider : BlobProviderBase, ITransientDependency |
|||
{ |
|||
protected IBunnyBlobNameCalculator BunnyBlobNameCalculator { get; } |
|||
protected IBlobNormalizeNamingService BlobNormalizeNamingService { get; } |
|||
protected IBunnyClientFactory BunnyClientFactory { get; } |
|||
|
|||
public BunnyBlobProvider( |
|||
IBunnyBlobNameCalculator bunnyBlobNameCalculator, |
|||
IBlobNormalizeNamingService blobNormalizeNamingService, |
|||
IBunnyClientFactory bunnyClientFactory) |
|||
{ |
|||
BunnyBlobNameCalculator = bunnyBlobNameCalculator; |
|||
BlobNormalizeNamingService = blobNormalizeNamingService; |
|||
BunnyClientFactory = bunnyClientFactory; |
|||
} |
|||
|
|||
public async override Task SaveAsync(BlobProviderSaveArgs args) |
|||
{ |
|||
var configuration = args.Configuration.GetBunnyConfiguration(); |
|||
var containerName = GetContainerName(args); |
|||
var blobName = BunnyBlobNameCalculator.Calculate(args); |
|||
|
|||
await ValidateContainerExistsAsync(containerName, configuration); |
|||
|
|||
var bunnyStorage = await GetBunnyCDNStorageAsync(args); |
|||
|
|||
if (!args.OverrideExisting && await BlobExistsAsync(bunnyStorage, containerName, blobName)) |
|||
{ |
|||
throw new BlobAlreadyExistsException( |
|||
$"Blob '{args.BlobName}' already exists in container '{containerName}'. " + |
|||
$"Set {nameof(args.OverrideExisting)} to true to overwrite."); |
|||
} |
|||
|
|||
using var memoryStream = new MemoryStream(); |
|||
await args.BlobStream.CopyToAsync(memoryStream); |
|||
memoryStream.Position = 0; |
|||
|
|||
await bunnyStorage.UploadAsync(memoryStream, $"{containerName}/{blobName}"); |
|||
} |
|||
|
|||
public async override Task<bool> DeleteAsync(BlobProviderDeleteArgs args) |
|||
{ |
|||
var blobName = BunnyBlobNameCalculator.Calculate(args); |
|||
var containerName = GetContainerName(args); |
|||
var bunnyStorage = await GetBunnyCDNStorageAsync(args); |
|||
|
|||
if (!await BlobExistsAsync(bunnyStorage, containerName, blobName)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
return await bunnyStorage.DeleteObjectAsync($"{containerName}/{blobName}"); |
|||
} |
|||
catch (BunnyCDNStorageException ex) when (ex.Message.Contains("404")) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
public async override Task<bool> ExistsAsync(BlobProviderExistsArgs args) |
|||
{ |
|||
var blobName = BunnyBlobNameCalculator.Calculate(args); |
|||
var containerName = GetContainerName(args); |
|||
var bunnyStorage = await GetBunnyCDNStorageAsync(args); |
|||
|
|||
return await BlobExistsAsync(bunnyStorage, containerName, blobName); |
|||
} |
|||
|
|||
public async override Task<Stream?> GetOrNullAsync(BlobProviderGetArgs args) |
|||
{ |
|||
var blobName = BunnyBlobNameCalculator.Calculate(args); |
|||
var containerName = GetContainerName(args); |
|||
var bunnyStorage = await GetBunnyCDNStorageAsync(args); |
|||
|
|||
if (!await BlobExistsAsync(bunnyStorage, containerName, blobName)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
return await bunnyStorage.DownloadObjectAsStreamAsync($"{containerName}/{blobName}"); |
|||
} |
|||
catch (WebException ex) when ((HttpStatusCode)ex.Status == HttpStatusCode.NotFound) |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
protected virtual async Task<bool> BlobExistsAsync(BunnyCDNStorage bunnyStorage, string containerName, string blobName) |
|||
{ |
|||
try |
|||
{ |
|||
var fullBlobPath = $"/{containerName}/{blobName}"; |
|||
var directoryPath = Path.GetDirectoryName(fullBlobPath)?.Replace('\\', '/') + "/"; |
|||
|
|||
if (string.IsNullOrWhiteSpace(directoryPath)) |
|||
{ |
|||
throw new Exception("Invalid directory path generated from blob name."); |
|||
} |
|||
|
|||
var objects = await bunnyStorage.GetStorageObjectsAsync(directoryPath); |
|||
return objects?.Any(o => o.FullPath == fullBlobPath) == true; |
|||
} |
|||
catch (BunnyCDNStorageException ex) when (ex.Message.Contains("404")) |
|||
{ |
|||
return false; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
throw new Exception($"Error while checking blob existence: {ex.Message}", ex); |
|||
} |
|||
} |
|||
|
|||
protected virtual async Task<BunnyCDNStorage> GetBunnyCDNStorageAsync(BlobProviderArgs args) |
|||
{ |
|||
var configuration = args.Configuration.GetBunnyConfiguration(); |
|||
var containerName = GetContainerName(args); |
|||
var region = configuration.Region ?? "de"; |
|||
|
|||
return await BunnyClientFactory.CreateAsync( |
|||
configuration.AccessKey, |
|||
containerName, |
|||
region); |
|||
} |
|||
|
|||
protected virtual string GetContainerName(BlobProviderArgs args) |
|||
{ |
|||
var configuration = args.Configuration.GetBunnyConfiguration(); |
|||
return configuration.ContainerName.IsNullOrWhiteSpace() |
|||
? args.ContainerName |
|||
: BlobNormalizeNamingService.NormalizeContainerName(args.Configuration, configuration.ContainerName!); |
|||
} |
|||
|
|||
protected virtual async Task ValidateContainerExistsAsync( |
|||
string containerName, |
|||
BunnyBlobProviderConfiguration configuration |
|||
) |
|||
{ |
|||
try |
|||
{ |
|||
await BunnyClientFactory.EnsureStorageZoneExistsAsync( |
|||
configuration.AccessKey, |
|||
containerName, |
|||
configuration.Region ?? "de", |
|||
configuration.CreateContainerIfNotExists); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
throw new AbpException( |
|||
$"Failed to validate storage zone '{containerName}': {ex.Message}", |
|||
ex); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
namespace Volo.Abp.BlobStoring.Bunny; |
|||
|
|||
public class BunnyBlobProviderConfiguration |
|||
{ |
|||
public string? Region { |
|||
get => _containerConfiguration.GetConfigurationOrDefault(BunnyBlobProviderConfigurationNames.Region, "de"); |
|||
set => _containerConfiguration.SetConfiguration(BunnyBlobProviderConfigurationNames.Region, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// This name may only contain lowercase letters, numbers, and hyphens. (no spaces)
|
|||
/// The name must also be between 4 and 64 characters long.
|
|||
/// The name must be globaly unique
|
|||
/// If this parameter is not specified, the ContainerName of the <see cref="BlobProviderArgs"/> will be used.
|
|||
/// </summary>
|
|||
public string? ContainerName { |
|||
get => _containerConfiguration.GetConfigurationOrDefault<string>(BunnyBlobProviderConfigurationNames.ContainerName); |
|||
set => _containerConfiguration.SetConfiguration(BunnyBlobProviderConfigurationNames.ContainerName, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Default value: false.
|
|||
/// </summary>
|
|||
public bool CreateContainerIfNotExists { |
|||
get => _containerConfiguration.GetConfigurationOrDefault(BunnyBlobProviderConfigurationNames.CreateContainerIfNotExists, false); |
|||
set => _containerConfiguration.SetConfiguration(BunnyBlobProviderConfigurationNames.CreateContainerIfNotExists, value); |
|||
} |
|||
|
|||
public string AccessKey { |
|||
get => _containerConfiguration.GetConfiguration<string>(BunnyBlobProviderConfigurationNames.AccessKey); |
|||
set => _containerConfiguration.SetConfiguration(BunnyBlobProviderConfigurationNames.AccessKey, value); |
|||
} |
|||
|
|||
private readonly BlobContainerConfiguration _containerConfiguration; |
|||
|
|||
public BunnyBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration) |
|||
{ |
|||
_containerConfiguration = containerConfiguration; |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
namespace Volo.Abp.BlobStoring.Bunny; |
|||
|
|||
public static class BunnyBlobProviderConfigurationNames |
|||
{ |
|||
// The primary region for the storage zone (e.g., DE, NY, etc.)
|
|||
public const string Region = "Bunny.Region"; |
|||
|
|||
// The name of the storage zone
|
|||
public const string ContainerName = "Bunny.ContainerName"; |
|||
|
|||
// The API access key for the bunny.net account
|
|||
public const string AccessKey = "Bunny.AccessKey"; |
|||
|
|||
public const string CreateContainerIfNotExists = "Bunny.CreateContainerIfNotExists"; |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.BlobStoring.Bunny; |
|||
|
|||
[Serializable] |
|||
public class BunnyStorageZoneModel |
|||
{ |
|||
public int Id { get; set; } |
|||
|
|||
public string Password { get; set; } = null!; |
|||
|
|||
public string Name { get; set; } = null!; |
|||
|
|||
public string? Region { get; set; } |
|||
|
|||
public bool Deleted { get; set; } |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace Volo.Abp.BlobStoring.Bunny; |
|||
|
|||
public class DefaultBunnyBlobNameCalculator : IBunnyBlobNameCalculator, ITransientDependency |
|||
{ |
|||
protected ICurrentTenant CurrentTenant { get; } |
|||
|
|||
public DefaultBunnyBlobNameCalculator(ICurrentTenant currentTenant) |
|||
{ |
|||
CurrentTenant = currentTenant; |
|||
} |
|||
|
|||
public virtual string Calculate(BlobProviderArgs args) |
|||
{ |
|||
return CurrentTenant.Id == null |
|||
? $"host/{args.BlobName}" |
|||
: $"tenants/{CurrentTenant.Id.Value.ToString("D")}/{args.BlobName}"; |
|||
} |
|||
} |
|||
@ -0,0 +1,152 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Net.Http; |
|||
using System.Text; |
|||
using System.Text.Json; |
|||
using System.Threading.Tasks; |
|||
using BunnyCDN.Net.Storage; |
|||
using Microsoft.Extensions.Caching.Distributed; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Security.Encryption; |
|||
|
|||
namespace Volo.Abp.BlobStoring.Bunny; |
|||
|
|||
public class DefaultBunnyClientFactory : IBunnyClientFactory, ITransientDependency |
|||
{ |
|||
private readonly IDistributedCache<BunnyStorageZoneModel> _cache; |
|||
private readonly IHttpClientFactory _httpClientFactory; |
|||
private readonly IStringEncryptionService _stringEncryptionService; |
|||
|
|||
private const string CacheKeyPrefix = "BunnyStorageZone:"; |
|||
private readonly static TimeSpan CacheDuration = TimeSpan.FromHours(12); |
|||
|
|||
public DefaultBunnyClientFactory( |
|||
IHttpClientFactory httpClient, |
|||
IDistributedCache<BunnyStorageZoneModel> cache, |
|||
IStringEncryptionService stringEncryptionService) |
|||
{ |
|||
_cache = cache; |
|||
_httpClientFactory = httpClient; |
|||
_stringEncryptionService = stringEncryptionService; |
|||
} |
|||
|
|||
public virtual async Task<BunnyCDNStorage> CreateAsync(string accessKey, string containerName, string region = "de") |
|||
{ |
|||
var cacheKey = $"{CacheKeyPrefix}{containerName}"; |
|||
var storageZoneInfo = await _cache.GetOrAddAsync( |
|||
cacheKey, |
|||
async () => { |
|||
var result = await GetStorageZoneAsync(accessKey, containerName); |
|||
if (result == null) |
|||
{ |
|||
throw new AbpException($"Storage zone '{containerName}' not found"); |
|||
} |
|||
|
|||
// Encrypt the sensitive password before caching
|
|||
result.Password = _stringEncryptionService.Encrypt(result.Password!)!; |
|||
return result; |
|||
}, |
|||
() => new DistributedCacheEntryOptions |
|||
{ |
|||
AbsoluteExpiration = DateTimeOffset.Now.Add(CacheDuration) |
|||
} |
|||
); |
|||
|
|||
if (storageZoneInfo == null) |
|||
{ |
|||
throw new AbpException($"Could not retrieve storage zone information for container '{containerName}'"); |
|||
} |
|||
|
|||
// Decrypt the password before using it
|
|||
var decryptedPassword = _stringEncryptionService.Decrypt(storageZoneInfo.Password); |
|||
|
|||
return new BunnyCDNStorage(containerName, decryptedPassword, region); |
|||
} |
|||
|
|||
public virtual async Task EnsureStorageZoneExistsAsync( |
|||
string accessKey, |
|||
string containerName, |
|||
string region = "de", |
|||
bool createIfNotExists = false) |
|||
{ |
|||
var storageZone = await GetStorageZoneAsync(accessKey, containerName); |
|||
|
|||
if (storageZone == null) |
|||
{ |
|||
if (!createIfNotExists) |
|||
{ |
|||
throw new AbpException( |
|||
$"Storage zone '{containerName}' does not exist. " + |
|||
"Set createIfNotExists to true to create it automatically."); |
|||
} |
|||
|
|||
await CreateStorageZoneAsync(accessKey, containerName, region); |
|||
|
|||
// Clear the cache to force a refresh of the storage zone info
|
|||
var cacheKey = $"{CacheKeyPrefix}{containerName}"; |
|||
await _cache.RemoveAsync(cacheKey); |
|||
} |
|||
} |
|||
|
|||
protected virtual async Task<BunnyStorageZoneModel> CreateStorageZoneAsync( |
|||
string accessKey, |
|||
string containerName, |
|||
string region) |
|||
{ |
|||
using (var client = _httpClientFactory.CreateClient("BunnyApiClient")) |
|||
{ |
|||
client.DefaultRequestHeaders.Add("AccessKey", accessKey); |
|||
|
|||
var payload = new Dictionary<string, object> |
|||
{ |
|||
{ "Name", containerName }, |
|||
{ "Region", region }, |
|||
{ "ZoneTier", 0 } |
|||
}; |
|||
|
|||
var content = new StringContent( |
|||
JsonSerializer.Serialize(payload), |
|||
Encoding.UTF8, |
|||
"application/json"); |
|||
|
|||
var response = await client.PostAsync( |
|||
"https://api.bunny.net/storagezone", |
|||
content); |
|||
|
|||
if (!response.IsSuccessStatusCode) |
|||
{ |
|||
var errorContent = await response.Content.ReadAsStringAsync(); |
|||
throw new AbpException( |
|||
$"Failed to create storage zone '{containerName}'. " + |
|||
$"Status: {response.StatusCode}, Error: {errorContent}"); |
|||
} |
|||
|
|||
var responseContent = await response.Content.ReadAsStringAsync(); |
|||
var createdZone = JsonSerializer.Deserialize<BunnyStorageZoneModel>(responseContent); |
|||
|
|||
if (createdZone == null) |
|||
{ |
|||
throw new AbpException($"Failed to deserialize the created storage zone response for '{containerName}'"); |
|||
} |
|||
|
|||
return createdZone; |
|||
} |
|||
} |
|||
|
|||
protected virtual async Task<BunnyStorageZoneModel?> GetStorageZoneAsync(string accessKey, string containerName) |
|||
{ |
|||
using (var client = _httpClientFactory.CreateClient("BunnyApiClient")) |
|||
{ |
|||
client.DefaultRequestHeaders.Add("AccessKey", accessKey); |
|||
var response = await client.GetAsync("https://api.bunny.net/storagezone"); |
|||
response.EnsureSuccessStatusCode(); |
|||
|
|||
var content = await response.Content.ReadAsStringAsync(); |
|||
var zones = JsonSerializer.Deserialize<BunnyStorageZoneModel[]>(content); |
|||
|
|||
return zones?.FirstOrDefault(x => x.Name.Equals(containerName, StringComparison.OrdinalIgnoreCase) && !x.Deleted); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Volo.Abp.BlobStoring.Bunny; |
|||
|
|||
public interface IBunnyBlobNameCalculator |
|||
{ |
|||
string Calculate(BlobProviderArgs args); |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System.Threading.Tasks; |
|||
using BunnyCDN.Net.Storage; |
|||
|
|||
namespace Volo.Abp.BlobStoring.Bunny; |
|||
|
|||
public interface IBunnyClientFactory |
|||
{ |
|||
Task<BunnyCDNStorage> CreateAsync(string accessKey, string containerName, string region = "de"); |
|||
|
|||
Task EnsureStorageZoneExistsAsync(string accessKey, string containerName, string region = "de", bool createIfNotExists = false); |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"role": "lib.test" |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.test.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net9.0</TargetFramework> |
|||
<RootNamespace /> |
|||
<UserSecretsId>9f0d2c00-80c1-435b-bfab-2c39c8249091</UserSecretsId> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Volo.Abp.BlobStoring.Bunny\Volo.Abp.BlobStoring.Bunny.csproj" /> |
|||
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" /> |
|||
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" /> |
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" /> |
|||
<ProjectReference Include="..\Volo.Abp.BlobStoring.Tests\Volo.Abp.BlobStoring.Tests.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||