diff --git a/Directory.Packages.props b/Directory.Packages.props
index 251b2cf00d..9c944083b9 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -15,6 +15,7 @@
+
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
index e48a677ee7..d53323c3a2 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
@@ -685,6 +685,9 @@
"RedisManagement": "Redis Management",
"Permission:RedisManagement": "Redis Management",
"UserCleanUp": "User Clean Up",
- "Permission:UserCleanUp": "User Clean Up"
+ "Permission:UserCleanUp": "User Clean Up",
+ "AllowPrivateQuestion": "Allow Private Question",
+ "Permission:Campaigns": "Campaigns",
+ "Permission:Licenses": "License Settings"
}
}
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
index 05e4959514..2cc7827122 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
@@ -586,10 +586,10 @@
"CreatePostSummaryInfo": "A short summary of the post to be shown on the post list. Maximum length: {0}",
"CreatePostCoverInfo": "For creating an effective post, add a cover photo. Upload 16:9 aspect ratio pictures for the best view.
Maximum file size: 1MB.",
"CreatePostCoverInfo_Title": "Add a cover image to your post.",
- "CreatePostCoverInfo1": " Accepted file types : JPEG, JPG, PNG",
- "CreatePostCoverInfo2": " Max file size : 1 MB",
- "CreatePostCoverInfo3": " Image proportion : 16:9",
- "CreatePostCoverInfo4": " Download a sample cover image ",
+ "CreatePostCoverInfo1": "Accepted file types : JPEG, JPG, PNG",
+ "CreatePostCoverInfo2": "Max file size : 1 MB",
+ "CreatePostCoverInfo3": "Image proportion : 16:9",
+ "CreatePostCoverInfo4": " Download a sample cover image ",
"ThisExtensionIsNotAllowed": "This extension is not allowed.",
"TheFileIsTooLarge": "The file is too large.",
"GoToThePost": "Go to the Post",
diff --git a/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/POST.md b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/POST.md
index c07509d2e3..4706e79f49 100644
--- a/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/POST.md
+++ b/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/POST.md
@@ -62,6 +62,8 @@ dotnet add package Microsoft.Extensions.AI.OpenAI --prerelease
> Replace the value of the `Key` with your OpenAI API key.
+> **Important Security Note**: Storing sensitive information like API keys in `appsettings.json` is not recommended due to security concerns. Please refer to the [official Microsoft documentation](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) for secure secret management best practices.
+
Next, add the following code to the `ConfigureServices` method in `OpenAIIntegrationBlazorModule`:
```csharp
diff --git a/docs/en/Community-Articles/2025-01-24-Understanding-Transactions-in-ABP-Unit-Of-Work/POST.md b/docs/en/Community-Articles/2025-01-24-Understanding-Transactions-in-ABP-Unit-Of-Work/POST.md
new file mode 100644
index 0000000000..49a75b2a25
--- /dev/null
+++ b/docs/en/Community-Articles/2025-01-24-Understanding-Transactions-in-ABP-Unit-Of-Work/POST.md
@@ -0,0 +1,327 @@
+# 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(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 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)
diff --git a/docs/en/Community-Articles/2025-01-24-Understanding-Transactions-in-ABP-Unit-Of-Work/pic.png b/docs/en/Community-Articles/2025-01-24-Understanding-Transactions-in-ABP-Unit-Of-Work/pic.png
new file mode 100644
index 0000000000..d6805a806e
Binary files /dev/null and b/docs/en/Community-Articles/2025-01-24-Understanding-Transactions-in-ABP-Unit-Of-Work/pic.png differ
diff --git a/docs/en/Community-Articles/2025-02-04-OpenIddict-Custom-Logic/POST.md b/docs/en/Community-Articles/2025-02-04-OpenIddict-Custom-Logic/POST.md
new file mode 100644
index 0000000000..f5602650ae
--- /dev/null
+++ b/docs/en/Community-Articles/2025-02-04-OpenIddict-Custom-Logic/POST.md
@@ -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`:
+
+```csharp
+using System.Threading.Tasks;
+using OpenIddict.Server;
+
+namespace MySolution;
+
+public class SignOutEventHandler : IOpenIddictServerHandler
+{
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .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(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/)
\ No newline at end of file
diff --git a/docs/en/Community-Articles/2025-02-04-OpenIddict-Custom-Logic/cover-image.png b/docs/en/Community-Articles/2025-02-04-OpenIddict-Custom-Logic/cover-image.png
new file mode 100644
index 0000000000..11d9ff05ea
Binary files /dev/null and b/docs/en/Community-Articles/2025-02-04-OpenIddict-Custom-Logic/cover-image.png differ
diff --git a/docs/en/Community-Articles/2025-02-04-OpenIddict-Custom-Logic/openiddict-server-events.png b/docs/en/Community-Articles/2025-02-04-OpenIddict-Custom-Logic/openiddict-server-events.png
new file mode 100644
index 0000000000..3514304f15
Binary files /dev/null and b/docs/en/Community-Articles/2025-02-04-OpenIddict-Custom-Logic/openiddict-server-events.png differ
diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json
index 8253b732df..8ca147a3db 100644
--- a/docs/en/docs-nav.json
+++ b/docs/en/docs-nav.json
@@ -1430,6 +1430,10 @@
"text": "LeptonX Lite",
"path": "ui-themes/lepton-x-lite/blazor.md"
},
+ {
+ "text": "LeptonX",
+ "path": "ui-themes/lepton-x/blazor.md"
+ },
{
"text": "Branding",
"path": "framework/ui/blazor/branding.md"
@@ -1751,6 +1755,10 @@
{
"text": "LeptonX Lite",
"path": "ui-themes/lepton-x-lite/angular.md"
+ },
+ {
+ "text": "LeptonX",
+ "path": "ui-themes/lepton-x/angular.md"
}
]
},
diff --git a/docs/en/framework/infrastructure/blob-storing/bunny.md b/docs/en/framework/infrastructure/blob-storing/bunny.md
new file mode 100644
index 0000000000..4c5fb5ef0f
--- /dev/null
+++ b/docs/en/framework/infrastructure/blob-storing/bunny.md
@@ -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(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/` 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.
diff --git a/docs/en/framework/infrastructure/blob-storing/index.md b/docs/en/framework/infrastructure/blob-storing/index.md
index f95a90e410..676757ad80 100644
--- a/docs/en/framework/infrastructure/blob-storing/index.md
+++ b/docs/en/framework/infrastructure/blob-storing/index.md
@@ -23,6 +23,7 @@ The ABP has already the following storage provider implementations:
* [Minio](./minio.md): Stores BLOBs on the [MinIO Object storage](https://min.io/).
* [Aws](./aws.md): Stores BLOBs on the [Amazon Simple Storage Service](https://aws.amazon.com/s3/).
* [Google](./google.md): Stores BLOBs on the [Google Cloud Storage](https://cloud.google.com/storage).
+* [Bunny](./bunny.md): Stores BLOBs on the [Bunny.net Storage](https://bunny.net/storage/).
More providers will be implemented by the time. You can [request](https://github.com/abpframework/abp/issues/new) it for your favorite provider or [create it yourself](./custom-provider.md) and [contribute](../../../contribution) to the ABP.
diff --git a/docs/en/framework/ui/blazor/overall.md b/docs/en/framework/ui/blazor/overall.md
index 12cef14552..54a545759e 100644
--- a/docs/en/framework/ui/blazor/overall.md
+++ b/docs/en/framework/ui/blazor/overall.md
@@ -1,14 +1,17 @@
# Blazor UI: Overall
-## Introduction
+[Blazor](https://docs.microsoft.com/en-us/aspnet/core/blazor/) is a framework for building interactive client-side web UI with .NET. It enables .NET developers to create Single-Page Web Applications using C# and the Razor syntax.
-[Blazor](https://docs.microsoft.com/en-us/aspnet/core/blazor/) is a framework for building interactive client-side web UI with .NET. It is promising for a .NET developer that you can create Single-Page Web Applications using C# and the Razor syntax.
+ABP provides comprehensive infrastructure and integrations that make your Blazor development easier, comfortable and enjoyable. ABP supports multiple Blazor hosting models:
-ABP provides infrastructure and integrations that make your Blazor development even easier, comfortable and enjoyable.
+* **Blazor WebAssembly (WASM)**: Client-side hosting model where the entire application runs in the browser using WebAssembly
+* **Blazor Server**: Server-side hosting model with a real-time SignalR connection
+* **Blazor WebApp**: The new hybrid/united model introduced in .NET 8 combining the benefits of Server and WebAssembly approaches
+* **MAUI Blazor**: For building cross-platform native applications using Blazor & MAUI
-This document provides an overview for the ABP Blazor UI integration and highlights some major features.
+This document provides an overview of the ABP Blazor UI integration and highlights some major features.
-### Getting Started
+## Getting Started
You can follow the documents below to start with the ABP and the Blazor UI now:
@@ -94,7 +97,7 @@ These libraries are selected as the base libraries and available to the applicat
> Bootstrap's JavaScript part is not used since the Blazorise library already provides the necessary functionalities to the Bootstrap components in a native way.
-> Beginning from June, 2021, the Blazorise library has dual licenses; open source & commercial. Based on your yearly revenue, you may need to buy a commercial license. See [this post](https://blazorise.com/news/announcing-2022-blazorise-plans-and-pricing-updates) to learn more. The Blazorise license is bundled with ABP and commercial customers doesn’t need to buy an extra Blazorise license.
+> Beginning from June, 2021, the Blazorise library has dual licenses; open source & commercial. Based on your yearly revenue, you may need to buy a commercial license. See [this post](https://blazorise.com/news/announcing-2022-blazorise-plans-and-pricing-updates) to learn more. The Blazorise license is bundled with ABP and commercial customers doesn't need to buy an extra Blazorise license.
### The Layout
diff --git a/docs/en/framework/ui/blazor/theming.md b/docs/en/framework/ui/blazor/theming.md
index 0c7d6f8406..9a056580bb 100644
--- a/docs/en/framework/ui/blazor/theming.md
+++ b/docs/en/framework/ui/blazor/theming.md
@@ -27,7 +27,7 @@ Currently, three themes are **officially provided**:
* The [Basic Theme](basic-theme.md) is the minimalist theme with the plain Bootstrap style. It is **open source and free**.
* The [Lepton Theme](https://abp.io/themes) is a **commercial** theme developed by the core ABP team and is a part of the [ABP](https://abp.io/) license.
-* The [LeptonX Theme](https://x.leptontheme.com/) is a theme that has a [commercial](https://docs.abp.io/en/commercial/latest/themes/lepton-x/blazor) and a [lite](../../../ui-themes/lepton-x-lite/blazor.md) version.
+* The [LeptonX Theme](https://x.leptontheme.com/) is a theme that has a [commercial](../../../ui-themes/lepton-x/blazor.md) and a [lite](../../../ui-themes/lepton-x-lite/blazor.md) version.
## Overall
diff --git a/docs/en/framework/ui/mvc-razor-pages/tag-helpers/form-elements.md b/docs/en/framework/ui/mvc-razor-pages/tag-helpers/form-elements.md
index 9230075ed3..b59ecf8dd3 100644
--- a/docs/en/framework/ui/mvc-razor-pages/tag-helpers/form-elements.md
+++ b/docs/en/framework/ui/mvc-razor-pages/tag-helpers/form-elements.md
@@ -10,7 +10,7 @@ See the [form elements demo page](https://bootstrap-taghelpers.abp.io/Components
## abp-input
-`abp-input` tag creates a Bootstrap form input for a given c# property. It uses [Asp.Net Core Input Tag Helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-7.0#the-input-tag-helper) in background, so every data annotation attribute of `input` tag helper of Asp.Net Core is also valid for `abp-input`.
+`abp-input` tag creates a Bootstrap form input for a given c# property. It uses [Asp.Net Core Input Tag Helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-9.0#the-input-tag-helper) in background, so every data annotation attribute of `input` tag helper of Asp.Net Core is also valid for `abp-input`.
Usage:
@@ -89,7 +89,7 @@ You can set some of the attributes on your c# property, or directly on HTML tag.
* `required-symbol`: Adds the required symbol `(*)` to the label when the input is required. The default value is `True`.
* `floating-label`: Sets the label as floating label. The default value is `False`.
-`asp-format`, `name` and `value` attributes of [Asp.Net Core Input Tag Helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-7.0#the-input-tag-helper) are also valid for `abp-input` tag helper.
+`asp-format`, `name` and `value` attributes of [Asp.Net Core Input Tag Helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-9.0#the-input-tag-helper) are also valid for `abp-input` tag helper.
### Label & Localization
@@ -101,7 +101,7 @@ You can set the label of the input in several ways:
## abp-select
-`abp-select` tag creates a Bootstrap form select for a given c# property. It uses [ASP.NET Core Select Tag Helper](https://docs.microsoft.com/tr-tr/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-select-tag-helper) in background, so every data annotation attribute of `select` tag helper of ASP.NET Core is also valid for `abp-select`.
+`abp-select` tag creates a Bootstrap form select for a given c# property. It uses [ASP.NET Core Select Tag Helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-9.0#the-select-tag-helper) in background, so every data annotation attribute of `select` tag helper of ASP.NET Core is also valid for `abp-select`.
`abp-select` tag needs a list of `Microsoft.AspNetCore.Mvc.Rendering.SelectListItem ` to work. It can be provided by `asp-items` attriube on the tag or `[SelectItems()]` attribute on c# property. (if you are using [abp-dynamic-form](dynamic-forms.md), c# attribute is the only way.)
@@ -432,4 +432,4 @@ newPicker.insertAfter($('body'));
* `startDateName`: Sets the name of the hidden start date input.
* `endDateName`: Sets the name of the hidden end date input.
* `dateName`: Sets the name of the hidden date input.
-* Other [datepicker options](https://www.daterangepicker.com/#options). Eg: `startDate: "2020-01-01"`.
\ No newline at end of file
+* Other [datepicker options](https://www.daterangepicker.com/#options). Eg: `startDate: "2020-01-01"`.
diff --git a/docs/en/modules/account/idle-session-timeout.md b/docs/en/modules/account/idle-session-timeout.md
index 069e6db665..231ea31e7d 100644
--- a/docs/en/modules/account/idle-session-timeout.md
+++ b/docs/en/modules/account/idle-session-timeout.md
@@ -6,7 +6,7 @@ The `Idle Session Timeout` feature allows you to automatically log out users aft
You can enable/disable the `Idle Session Timeout` feature in the `Setting > Account > Idle Session Timeout` page.
-The default idle session timeout is 1 hour. You can change it by selecting a different value from the dropdown list or entering a custom value(in minutes).
+The default idle session timeout is 1 hour. You can change it by selecting a different value from the dropdown list or entering a custom value (in minutes).

diff --git a/docs/en/solution-templates/layered-web-application/deployment/azure-deployment/step3-deployment-github-action.md b/docs/en/solution-templates/layered-web-application/deployment/azure-deployment/step3-deployment-github-action.md
index 9aa9e5df83..d6533ef6ec 100644
--- a/docs/en/solution-templates/layered-web-application/deployment/azure-deployment/step3-deployment-github-action.md
+++ b/docs/en/solution-templates/layered-web-application/deployment/azure-deployment/step3-deployment-github-action.md
@@ -295,7 +295,7 @@ jobs:
environment:
name: 'Production'
url: ${{ steps.deploy-to-webapp-3.outputs.webapp-url }}
-
+ steps:
- name: Download artifact from apihost
uses: actions/download-artifact@v4
with:
diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln
index c3ca5b63b2..ca471bb304 100644
--- a/framework/Volo.Abp.sln
+++ b/framework/Volo.Abp.sln
@@ -470,6 +470,7 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling", "src\Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling\Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling.csproj", "{2F9BA650-395C-4BE0-8CCB-9978E753562A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling", "src\Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling\Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling.csproj", "{7ADB6D92-82CC-4A2A-8BCF-FC6C6308796D}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Google", "src\Volo.Abp.BlobStoring.Google\Volo.Abp.BlobStoring.Google.csproj", "{DEEB5200-BBF9-464D-9B7E-8FC035A27E94}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Google.Tests", "test\Volo.Abp.BlobStoring.Google.Tests\Volo.Abp.BlobStoring.Google.Tests.csproj", "{40FB8907-9CF7-44D0-8B5F-538AC6DAF8B9}"
@@ -480,6 +481,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Sms.TencentCloud",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Sms.TencentCloud.Tests", "test\Volo.Abp.Sms.TencenCloud.Tests\Volo.Abp.Sms.TencentCloud.Tests.csproj", "{C753DDD6-5699-45F8-8669-08CE0BB816DE}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Bunny", "src\Volo.Abp.BlobStoring.Bunny\Volo.Abp.BlobStoring.Bunny.csproj", "{1BBCBA72-CDB6-4882-96EE-D4CD149433A2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Bunny.Tests", "test\Volo.Abp.BlobStoring.Bunny.Tests\Volo.Abp.BlobStoring.Bunny.Tests.csproj", "{BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1434,6 +1439,14 @@ Global
{C753DDD6-5699-45F8-8669-08CE0BB816DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C753DDD6-5699-45F8-8669-08CE0BB816DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C753DDD6-5699-45F8-8669-08CE0BB816DE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1BBCBA72-CDB6-4882-96EE-D4CD149433A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1BBCBA72-CDB6-4882-96EE-D4CD149433A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1BBCBA72-CDB6-4882-96EE-D4CD149433A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1BBCBA72-CDB6-4882-96EE-D4CD149433A2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1676,6 +1689,8 @@ Global
{E50739A7-5E2F-4EB5-AEA9-554115CB9613} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{BE7109C5-7368-4688-8557-4A15D3F4776A} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{C753DDD6-5699-45F8-8669-08CE0BB816DE} = {447C8A77-E5F0-4538-8687-7383196D04EA}
+ {1BBCBA72-CDB6-4882-96EE-D4CD149433A2} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
+ {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915} = {447C8A77-E5F0-4538-8687-7383196D04EA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs
index 9d1bac7f5f..115d6eeb02 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs
@@ -106,11 +106,11 @@ public class AbpPaginationTagHelperService : AbpTagHelperService GetPreviousButtonAsync(TagHelperContext context, TagHelperOutput output)
{
var localizationKey = "PagerPrevious";
- var currentPage = TagHelper.Model.CurrentPage == 1
- ? TagHelper.Model.CurrentPage.ToString()
- : (TagHelper.Model.CurrentPage - 1).ToString();
+ var currentPage = TagHelper.Model.CurrentPage > 1
+ ? (TagHelper.Model.CurrentPage - 1).ToString()
+ : "1";
return
- "\r\n" +
+ "\r\n" +
(await RenderAnchorTagHelperLinkHtmlAsync(context, output, currentPage, localizationKey)) + " ";
}
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/FodyWeavers.xml b/framework/src/Volo.Abp.BlobStoring.Bunny/FodyWeavers.xml
new file mode 100644
index 0000000000..1715698ccd
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/FodyWeavers.xsd b/framework/src/Volo.Abp.BlobStoring.Bunny/FodyWeavers.xsd
new file mode 100644
index 0000000000..ffa6fc4b78
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.abppkg b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.abppkg
new file mode 100644
index 0000000000..f4bad072d2
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.abppkg
@@ -0,0 +1,3 @@
+{
+ "role": "lib.framework"
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.abppkg.analyze.json b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.abppkg.analyze.json
new file mode 100644
index 0000000000..b9e8bbba1b
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.abppkg.analyze.json
@@ -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
+ }
+ ]
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.csproj b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.csproj
new file mode 100644
index 0000000000..2cd9c74ac6
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo.Abp.BlobStoring.Bunny.csproj
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ netstandard2.0;netstandard2.1;net8.0;net9.0
+ enable
+ Nullable
+ false
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/AbpBunnyBlobStoringModule .cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/AbpBunnyBlobStoringModule .cs
new file mode 100644
index 0000000000..3fe814f2f0
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/AbpBunnyBlobStoringModule .cs
@@ -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();
+ }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyApiException.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyApiException.cs
new file mode 100644
index 0000000000..1e10d4caa2
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyApiException.cs
@@ -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)
+ {
+
+ }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainerConfigurationExtensions.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainerConfigurationExtensions.cs
new file mode 100644
index 0000000000..03afe1c36d
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainerConfigurationExtensions.cs
@@ -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 bunnyConfigureAction)
+ {
+ containerConfiguration.ProviderType = typeof(BunnyBlobProvider);
+ containerConfiguration.NamingNormalizers.TryAdd();
+
+ bunnyConfigureAction(new BunnyBlobProviderConfiguration(containerConfiguration));
+
+ return containerConfiguration;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobNamingNormalizer.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobNamingNormalizer.cs
new file mode 100644
index 0000000000..74c436e2e9
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobNamingNormalizer.cs
@@ -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;
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProvider.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProvider.cs
new file mode 100644
index 0000000000..6c06bc3f95
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProvider.cs
@@ -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 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 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 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 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 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);
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProviderConfiguration.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProviderConfiguration.cs
new file mode 100644
index 0000000000..0897c7f2b8
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProviderConfiguration.cs
@@ -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);
+ }
+
+ ///
+ /// 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 will be used.
+ ///
+ public string? ContainerName {
+ get => _containerConfiguration.GetConfigurationOrDefault(BunnyBlobProviderConfigurationNames.ContainerName);
+ set => _containerConfiguration.SetConfiguration(BunnyBlobProviderConfigurationNames.ContainerName, value);
+ }
+
+ ///
+ /// Default value: false.
+ ///
+ public bool CreateContainerIfNotExists {
+ get => _containerConfiguration.GetConfigurationOrDefault(BunnyBlobProviderConfigurationNames.CreateContainerIfNotExists, false);
+ set => _containerConfiguration.SetConfiguration(BunnyBlobProviderConfigurationNames.CreateContainerIfNotExists, value);
+ }
+
+ public string AccessKey {
+ get => _containerConfiguration.GetConfiguration(BunnyBlobProviderConfigurationNames.AccessKey);
+ set => _containerConfiguration.SetConfiguration(BunnyBlobProviderConfigurationNames.AccessKey, value);
+ }
+
+ private readonly BlobContainerConfiguration _containerConfiguration;
+
+ public BunnyBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration)
+ {
+ _containerConfiguration = containerConfiguration;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProviderConfigurationNames.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProviderConfigurationNames.cs
new file mode 100644
index 0000000000..09d79076a6
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyBlobProviderConfigurationNames.cs
@@ -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";
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyStorageZoneModel.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyStorageZoneModel.cs
new file mode 100644
index 0000000000..e718531927
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/BunnyStorageZoneModel.cs
@@ -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; }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/DefaultBunnyBlobNameCalculator.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/DefaultBunnyBlobNameCalculator.cs
new file mode 100644
index 0000000000..f06acb8c14
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/DefaultBunnyBlobNameCalculator.cs
@@ -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}";
+ }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/DefaultBunnyClientFactory.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/DefaultBunnyClientFactory.cs
new file mode 100644
index 0000000000..d22cff0e0a
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/DefaultBunnyClientFactory.cs
@@ -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 _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 cache,
+ IStringEncryptionService stringEncryptionService)
+ {
+ _cache = cache;
+ _httpClientFactory = httpClient;
+ _stringEncryptionService = stringEncryptionService;
+ }
+
+ public virtual async Task 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 CreateStorageZoneAsync(
+ string accessKey,
+ string containerName,
+ string region)
+ {
+ using (var client = _httpClientFactory.CreateClient("BunnyApiClient"))
+ {
+ client.DefaultRequestHeaders.Add("AccessKey", accessKey);
+
+ var payload = new Dictionary
+ {
+ { "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(responseContent);
+
+ if (createdZone == null)
+ {
+ throw new AbpException($"Failed to deserialize the created storage zone response for '{containerName}'");
+ }
+
+ return createdZone;
+ }
+ }
+
+ protected virtual async Task 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(content);
+
+ return zones?.FirstOrDefault(x => x.Name.Equals(containerName, StringComparison.OrdinalIgnoreCase) && !x.Deleted);
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyBlobNameCalculator.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyBlobNameCalculator.cs
new file mode 100644
index 0000000000..34a18aca46
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyBlobNameCalculator.cs
@@ -0,0 +1,6 @@
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public interface IBunnyBlobNameCalculator
+{
+ string Calculate(BlobProviderArgs args);
+}
diff --git a/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyClientFactory.cs b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyClientFactory.cs
new file mode 100644
index 0000000000..7e5db5ed41
--- /dev/null
+++ b/framework/src/Volo.Abp.BlobStoring.Bunny/Volo/Abp/BlobStoring/Bunny/IBunnyClientFactory.cs
@@ -0,0 +1,11 @@
+using System.Threading.Tasks;
+using BunnyCDN.Net.Storage;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public interface IBunnyClientFactory
+{
+ Task CreateAsync(string accessKey, string containerName, string region = "de");
+
+ Task EnsureStorageZoneExistsAsync(string accessKey, string containerName, string region = "de", bool createIfNotExists = false);
+}
diff --git a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs
index 6d809a73bc..92134e5c97 100644
--- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs
+++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs
@@ -139,15 +139,16 @@ public class UnitOfWork : IUnitOfWork, ITransientDependency
_isCompleting = true;
await SaveChangesAsync(cancellationToken);
- DistributedEvents.AddRange(GetEventsRecords(DistributedEventWithPredicates));
LocalEvents.AddRange(GetEventsRecords(LocalEventWithPredicates));
+ LocalEventWithPredicates.Clear();
+ DistributedEvents.AddRange(GetEventsRecords(DistributedEventWithPredicates));
+ DistributedEventWithPredicates.Clear();
while (LocalEvents.Any() || DistributedEvents.Any())
{
if (LocalEvents.Any())
{
var localEventsToBePublished = LocalEvents.OrderBy(e => e.EventOrder).ToArray();
- LocalEventWithPredicates.Clear();
LocalEvents.Clear();
await UnitOfWorkEventPublisher.PublishLocalEventsAsync(
localEventsToBePublished
@@ -157,7 +158,6 @@ public class UnitOfWork : IUnitOfWork, ITransientDependency
if (DistributedEvents.Any())
{
var distributedEventsToBePublished = DistributedEvents.OrderBy(e => e.EventOrder).ToArray();
- DistributedEventWithPredicates.Clear();
DistributedEvents.Clear();
await UnitOfWorkEventPublisher.PublishDistributedEventsAsync(
distributedEventsToBePublished
@@ -167,7 +167,9 @@ public class UnitOfWork : IUnitOfWork, ITransientDependency
await SaveChangesAsync(cancellationToken);
LocalEvents.AddRange(GetEventsRecords(LocalEventWithPredicates));
+ LocalEventWithPredicates.Clear();
DistributedEvents.AddRange(GetEventsRecords(DistributedEventWithPredicates));
+ DistributedEventWithPredicates.Clear();
}
await CommitTransactionsAsync(cancellationToken);
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo.Abp.BlobStoring.Bunny.Tests.abppkg b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo.Abp.BlobStoring.Bunny.Tests.abppkg
new file mode 100644
index 0000000000..a686451fbc
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo.Abp.BlobStoring.Bunny.Tests.abppkg
@@ -0,0 +1,3 @@
+{
+ "role": "lib.test"
+}
\ No newline at end of file
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo.Abp.BlobStoring.Bunny.Tests.csproj b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo.Abp.BlobStoring.Bunny.Tests.csproj
new file mode 100644
index 0000000000..408f630fe2
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo.Abp.BlobStoring.Bunny.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+
+ net9.0
+
+ 9f0d2c00-80c1-435b-bfab-2c39c8249091
+
+
+
+
+
+
+
+
+
+
+
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestBase.cs b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestBase.cs
new file mode 100644
index 0000000000..4f37cfca91
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestBase.cs
@@ -0,0 +1,19 @@
+using Volo.Abp.Testing;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public class AbpBlobStoringBunnyTestCommonBase : AbpIntegratedTest
+{
+ protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
+ {
+ options.UseAutofac();
+ }
+}
+
+public class AbpBlobStoringBunnyTestBase : AbpIntegratedTest
+{
+ protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
+ {
+ options.UseAutofac();
+ }
+}
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestModule.cs b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestModule.cs
new file mode 100644
index 0000000000..73066291ed
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/AbpBlobStoringBunnyTestModule.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute.Extensions;
+using Volo.Abp.Modularity;
+using Volo.Abp.Threading;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+///
+/// This module will not try to connect to Bunny.
+///
+[DependsOn(
+ typeof(AbpBlobStoringBunnyModule),
+ typeof(AbpBlobStoringTestModule)
+)]
+public class AbpBlobStoringBunnyTestCommonModule : AbpModule
+{
+}
+
+[DependsOn(
+ typeof(AbpBlobStoringBunnyTestCommonModule)
+)]
+public class AbpBlobStoringBunnyTestModule : AbpModule
+{
+ private const string UserSecretsId = "9f0d2c00-80c1-435b-bfab-2c39c8249091";
+
+ private readonly string _randomContainerName = "abp-bunny-test-container-" + Guid.NewGuid().ToString("N");
+
+ private BunnyBlobProviderConfiguration _configuration;
+
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(builderAction: builder =>
+ {
+ builder.AddUserSecrets(UserSecretsId);
+ }));
+
+ var configuration = context.Services.GetConfiguration();
+ var accessKey = configuration["Bunny:AccessKey"];
+ var region = configuration["Bunny:Region"];
+
+ Configure(options =>
+ {
+ options.Containers.ConfigureAll((containerName, containerConfiguration) =>
+ {
+ containerConfiguration.UseBunny(bunny =>
+ {
+ bunny.AccessKey = accessKey;
+ bunny.Region = region;
+ bunny.CreateContainerIfNotExists = true;
+ bunny.ContainerName = _randomContainerName;
+
+ _configuration = bunny;
+ });
+ });
+ });
+ }
+}
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainer_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainer_Tests.cs
new file mode 100644
index 0000000000..84b255e4c5
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobContainer_Tests.cs
@@ -0,0 +1,12 @@
+namespace Volo.Abp.BlobStoring.Bunny;
+
+/*
+//Please set the correct connection string in secrets.json and continue the test.
+public class BunnyBlobContainer_Tests : BlobContainer_Tests
+{
+ public BunnyBlobContainer_Tests()
+ {
+
+ }
+}
+*/
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobNameCalculator_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobNameCalculator_Tests.cs
new file mode 100644
index 0000000000..fc3b2d8365
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/BunnyBlobNameCalculator_Tests.cs
@@ -0,0 +1,56 @@
+using System;
+using Shouldly;
+using Volo.Abp.MultiTenancy;
+using Xunit;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public class BunnyBlobNameCalculatorTests : AbpBlobStoringBunnyTestCommonBase
+{
+ private readonly IBunnyBlobNameCalculator _calculator;
+ private readonly ICurrentTenant _currentTenant;
+
+ private const string BunnyContainerName = "/";
+ private const string BunnySeparator = "/";
+
+ public BunnyBlobNameCalculatorTests()
+ {
+ _calculator = GetRequiredService();
+ _currentTenant = GetRequiredService();
+ }
+
+ [Fact]
+ public void Default_Settings()
+ {
+ _calculator.Calculate(
+ GetArgs("my-container", "my-blob")
+ ).ShouldBe($"host{BunnySeparator}my-blob");
+ }
+
+ [Fact]
+ public void Default_Settings_With_TenantId()
+ {
+ var tenantId = Guid.NewGuid();
+
+ using (_currentTenant.Change(tenantId))
+ {
+ _calculator.Calculate(
+ GetArgs("my-container", "my-blob")
+ ).ShouldBe($"tenants{BunnySeparator}{tenantId:D}{BunnySeparator}my-blob");
+ }
+ }
+
+ private static BlobProviderArgs GetArgs(
+ string containerName,
+ string blobName)
+ {
+ return new BlobProviderGetArgs(
+ containerName,
+ new BlobContainerConfiguration().UseBunny(x =>
+ {
+ x.ContainerName = containerName;
+ }),
+ blobName
+ );
+ }
+}
diff --git a/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/DefaultBunnyBlobNamingNormalizerProvider_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/DefaultBunnyBlobNamingNormalizerProvider_Tests.cs
new file mode 100644
index 0000000000..c4d3cd6878
--- /dev/null
+++ b/framework/test/Volo.Abp.BlobStoring.Bunny.Tests/Volo/Abp/BlobStoring/Bunny/DefaultBunnyBlobNamingNormalizerProvider_Tests.cs
@@ -0,0 +1,57 @@
+using Shouldly;
+using Xunit;
+
+namespace Volo.Abp.BlobStoring.Bunny;
+
+public class DefaultBunnyBlobNamingNormalizerProviderTests : AbpBlobStoringBunnyTestCommonBase
+{
+ private readonly IBlobNamingNormalizer _blobNamingNormalizer;
+
+ public DefaultBunnyBlobNamingNormalizerProviderTests()
+ {
+ _blobNamingNormalizer = GetRequiredService();
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Lowercase()
+ {
+ var filename = "ThisIsMyContainerName";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.ShouldBe("thisismycontainername");
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Only_Letters_Numbers_Dash_Dots()
+ {
+ var filename = ",./this-i,/s-my-c,/ont,/ai+*/=!@#$n^&*er.name+/";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.ShouldBe("this-is-my-containername");
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Min_Length()
+ {
+ var filename = "a";
+ Assert.Throws(()=>
+ {
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ });
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Max_Length()
+ {
+ var longName = new string('a', 65); // 65 characters
+ var exception = Assert.Throws(() =>
+ _blobNamingNormalizer.NormalizeContainerName(longName)
+ );
+ }
+
+ [Fact]
+ public void NormalizeContainerName_Dots()
+ {
+ var filename = ".this..is.-.my.container....name.";
+ filename = _blobNamingNormalizer.NormalizeContainerName(filename);
+ filename.ShouldBe("thisis-mycontainername");
+ }
+}
diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs
index e5e3ad5cb6..8ac64d40c9 100644
--- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs
+++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs
@@ -21,6 +21,7 @@ public abstract class DomainEvents_Tests : TestAppTestBase AppEntityWithNavigationsRepository;
protected readonly ILocalEventBus LocalEventBus;
protected readonly IDistributedEventBus DistributedEventBus;
+ protected readonly IUnitOfWorkManager UnitOfWorkManager;
protected DomainEvents_Tests()
{
@@ -28,6 +29,7 @@ public abstract class DomainEvents_Tests : TestAppTestBase>();
LocalEventBus = GetRequiredService();
DistributedEventBus = GetRequiredService();
+ UnitOfWorkManager = GetRequiredService();
}
[Fact]
@@ -176,6 +178,52 @@ public abstract class DomainEvents_Tests : TestAppTestBase(async data =>
+ {
+ event1Triggered = true;
+ await DistributedEventBus.PublishAsync(new MyCustomEventData3 { Value = "42" });
+ });
+
+ DistributedEventBus.Subscribe(async data =>
+ {
+ event2Triggered = true;
+ await LocalEventBus.PublishAsync(new MyCustomEventData4 { Value = "42" });
+ });
+
+ LocalEventBus.Subscribe(async data =>
+ {
+ event3Triggered = true;
+ });
+
+ DistributedEventBus.Subscribe(async data =>
+ {
+ event4Triggered = true;
+ });
+
+ //Act
+ using (var uow = UnitOfWorkManager.Begin(requiresNew: true))
+ {
+ await LocalEventBus.PublishAsync(new MyCustomEventData { Value = "42" });
+ await DistributedEventBus.PublishAsync(new MyCustomEventData2 { Value = "42" });
+ await uow.CompleteAsync();
+ }
+
+ //Assert
+ event1Triggered.ShouldBeTrue();
+ event2Triggered.ShouldBeTrue();
+ event3Triggered.ShouldBeTrue();
+ event4Triggered.ShouldBeTrue();
+ }
+
private class MyCustomEventData
{
public string Value { get; set; }
@@ -185,6 +233,16 @@ public abstract class DomainEvents_Tests : TestAppTestBase : TestAppTestBase
diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs
index f17f74b602..8da3237f83 100644
--- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs
+++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs
@@ -69,6 +69,7 @@ public interface IIdentityUserRepository : IBasicRepository
bool includeDetails = false,
Guid? roleId = null,
Guid? organizationUnitId = null,
+ Guid? id = null,
string userName = null,
string phoneNumber = null,
string emailAddress = null,
@@ -114,6 +115,7 @@ public interface IIdentityUserRepository : IBasicRepository
string filter = null,
Guid? roleId = null,
Guid? organizationUnitId = null,
+ Guid? id = null,
string userName = null,
string phoneNumber = null,
string emailAddress = null,
diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs
index 86682b85db..a623894625 100644
--- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs
+++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs
@@ -62,35 +62,35 @@ public class EfCoreIdentityUserRepository : EfCoreRepository()
- join role in dbContext.Roles on userRole.RoleId equals role.Id
- where userIds.Contains(userRole.UserId)
- group new
- {
- userRole.UserId,
- role.Name
- } by userRole.UserId
+ join role in dbContext.Roles on userRole.RoleId equals role.Id
+ where userIds.Contains(userRole.UserId)
+ group new {
+ userRole.UserId,
+ role.Name
+ } by userRole.UserId
into gp
- select new IdentityUserIdWithRoleNames
- {
- Id = gp.Key, RoleNames = gp.Select(x => x.Name).ToArray()
- }).ToListAsync(cancellationToken: cancellationToken);
+ select new IdentityUserIdWithRoleNames
+ {
+ Id = gp.Key,
+ RoleNames = gp.Select(x => x.Name).ToArray()
+ }).ToListAsync(cancellationToken: cancellationToken);
var orgUnitRoles = await (from userOu in dbContext.Set()
- join roleOu in dbContext.Set() on userOu.OrganizationUnitId equals roleOu.OrganizationUnitId
- join role in dbContext.Roles on roleOu.RoleId equals role.Id
- where userIds.Contains(userOu.UserId)
- group new
- {
- userOu.UserId,
- role.Name
- } by userOu.UserId
+ join roleOu in dbContext.Set() on userOu.OrganizationUnitId equals roleOu.OrganizationUnitId
+ join role in dbContext.Roles on roleOu.RoleId equals role.Id
+ where userIds.Contains(userOu.UserId)
+ group new {
+ userOu.UserId,
+ role.Name
+ } by userOu.UserId
into gp
- select new IdentityUserIdWithRoleNames
- {
- Id = gp.Key, RoleNames = gp.Select(x => x.Name).ToArray()
- }).ToListAsync(cancellationToken: cancellationToken);
+ select new IdentityUserIdWithRoleNames
+ {
+ Id = gp.Key,
+ RoleNames = gp.Select(x => x.Name).ToArray()
+ }).ToListAsync(cancellationToken: cancellationToken);
- return userRoles.Concat(orgUnitRoles).GroupBy(x => x.Id).Select(x => new IdentityUserIdWithRoleNames {Id = x.Key, RoleNames = x.SelectMany(y => y.RoleNames).Distinct().ToArray()}).ToList();
+ return userRoles.Concat(orgUnitRoles).GroupBy(x => x.Id).Select(x => new IdentityUserIdWithRoleNames { Id = x.Key, RoleNames = x.SelectMany(y => y.RoleNames).Distinct().ToArray() }).ToList();
}
public virtual async Task> GetRoleNamesInOrganizationUnitAsync(
@@ -196,6 +196,7 @@ public class EfCoreIdentityUserRepository : EfCoreRepository x.Id == id);
+ }
if (roleId.HasValue)
{
diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs
index c93a41e21d..64c47f8d2c 100644
--- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs
+++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs
@@ -164,6 +164,7 @@ public class MongoIdentityUserRepository : MongoDbRepository x.Roles.Select(r => r.RoleId)).ToList();
var allRoleIds = roleIds.Union(allOrganizationUnitRoleIds);
+
var roles = await (await GetQueryableAsync(cancellationToken)).Where(r => allRoleIds.Contains(r.Id)).Select(r => new{ r.Id, r.Name }).ToListAsync(cancellationToken);
var userRoles = userAndRoleIds.ToDictionary(x => x.Key, x => roles.Where(r => x.Value.Contains(r.Id)).Select(r => r.Name).ToArray());
@@ -412,9 +417,9 @@ public class MongoIdentityUserRepository : MongoDbRepository x.Id == id);
+ }
+
if (roleId.HasValue)
{
var organizationUnitIds = (await GetQueryableAsync(cancellationToken))
diff --git a/npm/ng-packs/packages/core/locale/src/utils/register-locale.ts b/npm/ng-packs/packages/core/locale/src/utils/register-locale.ts
index b3703d3400..1f061b5ada 100644
--- a/npm/ng-packs/packages/core/locale/src/utils/register-locale.ts
+++ b/npm/ng-packs/packages/core/locale/src/utils/register-locale.ts
@@ -55,7 +55,7 @@ export function registerLocaleForEsBuild(
const l = localeMap[locale] || locale;
const localeSupportList = "ar|cs|en|en-GB|es|de|fi|fr|hi|hu|is|it|pt|tr|ru|ro|sk|sl|zh-Hans|zh-Hant".split("|");
- if (localeSupportList.indexOf(locale) == -1) {
+ if (localeSupportList.indexOf(l) == -1) {
return;
}
return new Promise((resolve, reject) => {
@@ -96,6 +96,7 @@ export function registerLocale(
/* webpackChunkName: "locales"*/
/* webpackInclude: /[/\\](ar|cs|en|en-GB|es|de|fi|fr|hi|hu|is|it|pt|tr|ru|ro|sk|sl|zh-Hans|zh-Hant)\.(mjs|js)$/ */
/* webpackExclude: /[/\\]global|extra/ */
+ /* @vite-ignore */
`@angular/common${localePath}`
)
.then(val => {
diff --git a/npm/ng-packs/packages/schematics/src/utils/service.ts b/npm/ng-packs/packages/schematics/src/utils/service.ts
index b32dd5fba5..8f48ed3693 100644
--- a/npm/ng-packs/packages/schematics/src/utils/service.ts
+++ b/npm/ng-packs/packages/schematics/src/utils/service.ts
@@ -18,6 +18,7 @@ import {
createTypeAdapter,
createTypeParser,
createTypesToImportsReducer,
+ getTypeForEnumList,
removeTypeModifiers,
} from './type';
import { eBindingSourceId } from '../enums';
@@ -80,7 +81,19 @@ export function createActionToBodyMapper() {
const adaptType = createTypeAdapter();
return ({ httpMethod, parameters, returnValue, url }: Action) => {
- const responseType = adaptType(returnValue.typeSimple);
+ let responseType = adaptType(returnValue.typeSimple);
+ if (responseType.includes('enum')) {
+ const type = returnValue.typeSimple.replace('enum', returnValue.type);
+
+ if (responseType === 'enum') {
+ responseType = adaptType(type);
+ }
+
+ if (responseType === 'enum[]') {
+ const normalizedType = getTypeForEnumList(type);
+ responseType = adaptType(normalizedType);
+ }
+ }
const responseTypeWithNamespace = returnValue.typeSimple;
const body = new Body({ method: httpMethod, responseType, url, responseTypeWithNamespace });
@@ -109,7 +122,12 @@ export function createActionToSignatureMapper() {
if (isFormData || isFormArray) {
return new Property({ name: p.name, type: 'FormData' });
}
- const type = adaptType(p.typeSimple);
+
+ let type = adaptType(p.typeSimple);
+ if (p.typeSimple === 'enum' || p.typeSimple === '[enum]') {
+ type = adaptType(p.type);
+ }
+
const parameter = new Property({ name: p.name, type });
parameter.setDefault(p.defaultValue);
parameter.setOptional(p.isOptional);
@@ -183,7 +201,9 @@ function createActionToImportsReducer(
parseGenerics(paramType)
.toGenerics()
.forEach(type => {
- if (types[type]) acc.push({ type, isEnum: types[type].isEnum });
+ if (types[type]) {
+ acc.push({ type, isEnum: types[type].isEnum });
+ }
}),
);
diff --git a/npm/ng-packs/packages/schematics/src/utils/type.ts b/npm/ng-packs/packages/schematics/src/utils/type.ts
index 63c69c7aec..f6d16b39df 100644
--- a/npm/ng-packs/packages/schematics/src/utils/type.ts
+++ b/npm/ng-packs/packages/schematics/src/utils/type.ts
@@ -58,6 +58,10 @@ export function removeTypeModifiers(type: string) {
return type.replace(/\[\]/g, '');
}
+export function getTypeForEnumList(type: string) {
+ return type.replace(/^.*<([^>]+)>.*$/, '[$1]');
+}
+
export function createTypesToImportsReducer(solution: string, namespace: string) {
const mapTypeToImport = createTypeToImportMapper(solution, namespace);
@@ -68,7 +72,7 @@ export function createTypesToImportsReducer(solution: string, namespace: string)
return;
}
- if(newImport.specifiers.some(f => f.toLocaleLowerCase() === type.toLocaleLowerCase())){
+ if (newImport.specifiers.some(f => f.toLocaleLowerCase() === type.toLocaleLowerCase())) {
return;
}
@@ -76,7 +80,7 @@ export function createTypesToImportsReducer(solution: string, namespace: string)
({ keyword, path }) => keyword === newImport.keyword && path === newImport.path,
);
- if (!existingImport){
+ if (!existingImport) {
return imports.push(newImport);
}
@@ -101,7 +105,7 @@ export function createTypeToImportMapper(solution: string, namespace: string) {
const refs = [removeTypeModifiers(type)];
const specifiers = [adaptType(simplifyType(refs[0]).split('<')[0])];
let path = relativePathToModel(namespace, modelNamespace);
-
+
if (VOLO_REGEX.test(type)) {
path = '@abp/ng.core';
}
diff --git a/nupkg/common.ps1 b/nupkg/common.ps1
index 4233ce7b59..e60f374566 100644
--- a/nupkg/common.ps1
+++ b/nupkg/common.ps1
@@ -155,6 +155,7 @@ $projects = (
"framework/src/Volo.Abp.BlobStoring.Minio",
"framework/src/Volo.Abp.BlobStoring.Aws",
"framework/src/Volo.Abp.BlobStoring.Google",
+ "framework/src/Volo.Abp.BlobStoring.Bunny",
"framework/src/Volo.Abp.Caching",
"framework/src/Volo.Abp.Caching.StackExchangeRedis",
"framework/src/Volo.Abp.Castle.Core",