diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0a7d88c504..c695d81e41 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -7,7 +7,7 @@
-
+
@@ -137,7 +137,7 @@
-
+
@@ -196,4 +196,4 @@
-
\ No newline at end of file
+
diff --git a/README.md b/README.md
index fa6632daa8..4ef4bfab56 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@
- [Quick Start](https://abp.io/docs/latest/tutorials/todo) is a single-part, quick-start tutorial to build a simple application with the ABP Framework. Start with this tutorial if you want to understand how ABP works quickly.
- [Web Application Development Tutorial](https://abp.io/docs/latest/tutorials/book-store) is a complete tutorial on developing a full-stack web application with all aspects of a real-life solution.
- [Modular Monolith Application](https://abp.io/docs/latest/tutorials/modular-crm/index): A multi-part tutorial that demonstrates how to create application modules, compose and communicate them to build a monolith modular web application.
+- [Microservice Tutorial](https://abp.io/docs/latest/tutorials/microservice/index): A multi-part guide that walks you through building a microservice solution with ABP, from creating independent services and enabling inter-service communication to exposing them through an API Gateway and generating CRUD pages with ABP Suite.
## What ABP Provides?
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
index 622f30420c..16b93b9cc5 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
@@ -269,7 +269,7 @@
"Referral.CannotDeleteUsedLink": "You cannot delete a referral link that has already been used.",
"Referral.CannotReferYourself": "You cannot create a referral link for your own email address.",
"Referral:TargetEmail": "Target Email",
- "Referral.CannotReferSameOrganizationMember": "You cannot create a referral link for a user who is already a member of your organization.",
+ "Referral.CannotReferSameOrganizationMember": "Referral links cannot be used for existing organization members.",
"LinkCopiedToClipboard": "Link copied to clipboard",
"AreYouSureToDeleteReferralLink": "Are you sure you want to delete this referral link?",
"DefaultErrorMessage": "An error occurred."
diff --git a/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/cover.png b/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/cover.png
new file mode 100644
index 0000000000..9eee8f6d07
Binary files /dev/null and b/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/cover.png differ
diff --git a/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/images/architecture-diagram.svg b/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/images/architecture-diagram.svg
new file mode 100644
index 0000000000..d0c5fd900a
--- /dev/null
+++ b/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/images/architecture-diagram.svg
@@ -0,0 +1,145 @@
+
\ No newline at end of file
diff --git a/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/images/automatic-caching-flow.svg b/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/images/automatic-caching-flow.svg
new file mode 100644
index 0000000000..9ea54b1f46
--- /dev/null
+++ b/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/images/automatic-caching-flow.svg
@@ -0,0 +1,82 @@
+
\ No newline at end of file
diff --git a/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/images/cache-invalidation-flow.svg b/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/images/cache-invalidation-flow.svg
new file mode 100644
index 0000000000..d0281902d8
--- /dev/null
+++ b/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/images/cache-invalidation-flow.svg
@@ -0,0 +1,141 @@
+
\ No newline at end of file
diff --git a/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/images/cache-scoping-diagram.svg b/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/images/cache-scoping-diagram.svg
new file mode 100644
index 0000000000..848d9d0c51
--- /dev/null
+++ b/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/images/cache-scoping-diagram.svg
@@ -0,0 +1,135 @@
+
\ No newline at end of file
diff --git a/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/post.md b/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/post.md
new file mode 100644
index 0000000000..50a4aba33e
--- /dev/null
+++ b/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/post.md
@@ -0,0 +1,797 @@
+# Implement Automatic Method-Level Caching in ABP Framework
+
+Caching is one of the most effective ways to improve application performance, but implementing it manually for every method can be tedious and error-prone. What if you could cache method results automatically with just an attribute? In this article, we'll explore how to build an automatic method-level caching system in ABP Framework that handles cache invalidation, supports multiple scopes, and integrates seamlessly with your existing application.
+
+By the end of this guide, you'll understand how to implement attribute-based caching that automatically invalidates when entities change, supports user-specific and global caching scopes, and provides built-in metrics for monitoring cache performance.
+
+> 💡 **Complete Implementation Available**: This article is based on a working demo project. You can find the complete implementation in the [AbpAutoCacheDemo repository](https://github.com/salihozkara/AbpAutoCacheDemo), with the core AutoCache library implementation available in [this commit](https://github.com/salihozkara/AbpAutoCacheDemo/commit/946df1fc07de6eddd26eb14013a09968cd59329b).
+
+## What is Automatic Method-Level Caching?
+
+Automatic method-level caching is a technique that intercepts method calls and caches their results without requiring manual cache management code. Instead of writing cache logic in every method, you simply decorate methods with attributes that define caching behavior.
+
+
+
+The key benefits include:
+
+- **Reduced Boilerplate:** No repetitive cache management code in your business logic
+- **Consistent Caching Strategy:** Centralized cache configuration and behavior
+- **Smart Invalidation:** Automatic cache clearing when related entities change
+- **Multiple Scopes:** Support for global, user-specific, and entity-specific caching
+- **Built-in Monitoring:** Track cache hits, misses, and performance metrics
+
+## Architecture Overview
+
+The automatic caching system consists of several key components working together:
+
+
+
+**Core Components:**
+
+1. **CacheAttribute:** The attribute you apply to methods to enable automatic caching
+2. **AutoCacheInterceptor:** Intercepts method calls and handles cache operations
+3. **AutoCacheManager:** Manages cache storage, retrieval, and key generation
+4. **IAutoCacheKeyManager:** Handles cache key mapping and invalidation
+5. **AutoCacheInvalidationHandler:** Listens to entity changes and clears related caches
+
+This architecture leverages ABP's dynamic proxy system and event bus to provide seamless caching without modifying your business logic.
+
+## Prerequisites
+
+Before implementing automatic caching, ensure you have:
+
+- ABP Framework 10.0 or later
+## Implementation
+
+> 📦 **Repository Structure**: The complete implementation is available in the [AbpAutoCacheDemo repository](https://github.com/salihozkara/AbpAutoCacheDemo). The AutoCache library is located in the `src/AutoCache` folder, making it easy to extract and reuse in your own projects.
+
+### Step - 1: Create the AutoCache Module
+
+First, let's create a separate module for our caching infrastructure. This makes it reusable across projects.
+
+### Step - 1: Create the AutoCache Module
+
+First, let's create a separate module for our caching infrastructure. This makes it reusable across projects.
+
+Create `AutoCache.csproj`:
+
+```xml
+
+
+ net10.0
+ enable
+
+
+
+
+
+
+
+
+```
+
+Create the module class `AutoCacheModule.cs`:
+
+```csharp
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Caching.StackExchangeRedis;
+using Volo.Abp.Domain;
+using Volo.Abp.Modularity;
+
+namespace AutoCache;
+
+[DependsOn(typeof(AbpDddDomainModule), typeof(AbpCachingStackExchangeRedisModule))]
+public class AutoCacheModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.OnRegistered(AutoCacheRegister.RegisterInterceptorIfNeeded); // 👈 Register interceptor
+ }
+}
+```
+
+This module automatically registers the cache interceptor for any class that uses the `CacheAttribute`.
+
+### Step - 2: Define the Cache Attribute
+
+The `CacheAttribute` is the core of our automatic caching system. It specifies which entities affect the cache and what scope to use.
+
+Create `CacheAttribute.cs`:
+
+```csharp
+using System;
+using Volo.Abp.Domain.Entities;
+
+namespace AutoCache;
+
+[AttributeUsage(AttributeTargets.Method)]
+public class CacheAttribute : Attribute
+{
+ ///
+ /// Entity types that affect this cache. When these entities change, the cache will be invalidated.
+ ///
+ public Type[] InvalidateOnEntities { get; set; }
+
+ ///
+ /// Scope of the cache (Global, CurrentUser, AuthenticatedUser, or Entity)
+ ///
+ public AutoCacheScope Scope { get; set; } = AutoCacheScope.Global;
+
+ ///
+ /// Absolute expiration time relative to now in milliseconds (0 = use default, -1 = disabled)
+ ///
+ public long AbsoluteExpirationRelativeToNow { get; set; }
+
+ ///
+ /// Sliding expiration time in milliseconds (0 = use default, -1 = disabled)
+ ///
+ public long SlidingExpiration { get; set; }
+
+ public bool ConsiderUow { get; set; }
+
+ public string AdditionalCacheKey { get; set; }
+
+ public CacheAttribute(params Type[] invalidateOnEntities) // 👈 Specify entities that trigger cache invalidation
+ {
+ foreach (var entityType in invalidateOnEntities)
+ {
+ ArgumentNullException.ThrowIfNull(entityType);
+ if (!typeof(IEntity).IsAssignableFrom(entityType))
+ {
+ throw new ArgumentException($"Type {entityType.FullName} must implement IEntity interface.");
+ }
+ }
+ InvalidateOnEntities = invalidateOnEntities;
+ }
+}
+```
+
+**Key Properties:**
+
+- **InvalidateOnEntities:** Array of entity types that, when modified, will clear this cache
+- **Scope:** Determines cache visibility (Global, CurrentUser, AuthenticatedUser, Entity)
+- **AbsoluteExpirationRelativeToNow / SlidingExpiration:** Control cache lifetime
+
+### Step - 3: Define Cache Scopes
+
+Cache scopes determine how cache entries are partitioned. Create `AutoCacheScope.cs`:
+
+```csharp
+using System;
+
+namespace AutoCache;
+
+[Flags]
+public enum AutoCacheScope
+{
+ ///
+ /// Cache is shared globally across all users
+ ///
+ Global,
+
+ ///
+ /// Cache is scoped to the current user (based on user ID)
+ ///
+ CurrentUser,
+
+ ///
+ /// Cache is scoped to authenticated vs unauthenticated users
+ ///
+ AuthenticatedUser,
+
+ ///
+ /// Cache is scoped to the primary key of the entity involved
+ ///
+ Entity
+}
+```
+
+
+
+**When to Use Each Scope:**
+
+- **Global:** For data that's the same for all users (e.g., configuration, public lists)
+- **CurrentUser:** For user-specific data (e.g., user profile, user's orders)
+- **AuthenticatedUser:** For data that differs between authenticated and anonymous users
+- **Entity:** For data tied to a specific entity instance (e.g., book details by ID)
+
+### Step - 4: Implement the Cache Interceptor
+
+The interceptor is the heart of automatic caching. It intercepts method calls, checks the cache, and stores results. Create `AutoCacheInterceptor.cs`:
+
+```csharp
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.DynamicProxy;
+
+namespace AutoCache;
+
+public class AutoCacheInterceptor : AbpInterceptor, ITransientDependency
+{
+ private readonly ILogger _logger;
+ private readonly AutoCacheOptions _options;
+ private static readonly MethodInfo GetOrAddCacheAsyncMethod;
+ private readonly AutoCacheManager _autoCacheManager;
+ private static readonly ConcurrentDictionary MethodCache = new();
+
+ static AutoCacheInterceptor()
+ {
+ GetOrAddCacheAsyncMethod = typeof(AutoCacheInterceptor).GetMethod(
+ nameof(GetOrAddCacheAsync),
+ BindingFlags.NonPublic | BindingFlags.Instance
+ )!;
+ }
+
+ public AutoCacheInterceptor(
+ ILogger logger,
+ IOptions options,
+ AutoCacheManager autoCacheManager)
+ {
+ _logger = logger;
+ _autoCacheManager = autoCacheManager;
+ _options = options.Value;
+ }
+
+ public override async Task InterceptAsync(IAbpMethodInvocation invocation)
+ {
+ // Check if caching is enabled and method has [Cache] attribute
+ if(!_options.Enabled ||
+ invocation.Method.GetCustomAttributes(typeof(CacheAttribute), true).FirstOrDefault()
+ is not CacheAttribute attribute)
+ {
+ await invocation.ProceedAsync(); // 👈 No caching, proceed normally
+ return;
+ }
+
+ var proceeded = false;
+
+ try
+ {
+ // Create generic method based on return type
+ var genericMethod = MethodCache.GetOrAdd(invocation.Method.ReturnType, t =>
+ {
+ var isGenericTask = t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Task<>);
+ var resultType = isGenericTask ? t.GetGenericArguments()[0] : t;
+ return GetOrAddCacheAsyncMethod.MakeGenericMethod(resultType);
+ });
+
+ // Execute cache logic
+ (var result, proceeded) = await (Task<(object, bool)>)genericMethod.Invoke(this, [invocation, attribute])!;
+ invocation.ReturnValue = result; // 👈 Set cached or fresh result
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Error occurred while caching method {MethodName}", invocation.Method.Name);
+
+ if(e is AutoCacheExceptionWrapper exceptionWrapper)
+ {
+ if (_options.ThrowOnError)
+ {
+ throw exceptionWrapper.OriginalException;
+ }
+
+ _logger.LogWarning(
+ "Cache operation failed, falling back to method execution for {MethodName}",
+ invocation.Method.Name
+ );
+ }
+
+ if (!proceeded && invocation.ReturnValue == null)
+ {
+ await invocation.ProceedAsync(); // 👈 Fallback to actual method execution
+ }
+ }
+ }
+
+ private async Task<(object?, bool)> GetOrAddCacheAsync(
+ IAbpMethodInvocation invocation,
+ CacheAttribute attribute)
+ {
+ var proceeded = false;
+ var result = await _autoCacheManager.GetOrAddAsync(
+ invocation.TargetObject,
+ Factory,
+ invocation.Arguments,
+ () => new DistributedCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = GetExpiration(
+ attribute.AbsoluteExpirationRelativeToNow,
+ _options.DefaultAbsoluteExpirationRelativeToNow),
+ SlidingExpiration = GetExpiration(
+ attribute.SlidingExpiration,
+ _options.DefaultSlidingExpiration)
+ },
+ attribute.InvalidateOnEntities,
+ attribute.Scope,
+ attribute.ConsiderUow,
+ attribute.AdditionalCacheKey,
+ invocation.Method.Name);
+
+ return (result, proceeded);
+
+ async Task Factory()
+ {
+ await invocation.ProceedAsync(); // 👈 Execute actual method on cache miss
+ proceeded = true;
+ return (TResult)invocation.ReturnValue;
+ }
+ }
+
+ private static TimeSpan? GetExpiration(long milliseconds, long defaultValue)
+ {
+ return milliseconds switch
+ {
+ 0 => defaultValue > 0 ? TimeSpan.FromMilliseconds(defaultValue) : null,
+ < 0 => null,
+ _ => TimeSpan.FromMilliseconds(milliseconds)
+ };
+ }
+}
+```
+
+The interceptor intelligently determines whether to serve cached data or execute the actual method.
+
+### Step - 5: Implement the Cache Manager
+
+The `AutoCacheManager` handles the actual cache operations. Create a simplified version:
+
+```csharp
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Logging;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.DynamicProxy;
+using Volo.Abp.Users;
+
+namespace AutoCache;
+
+public class AutoCacheManager : IScopedDependency
+{
+ private readonly IAutoCacheKeyManager _autoCacheKeyManager;
+ private readonly ICurrentUser _currentUser;
+ private readonly ILogger _logger;
+ private readonly IAutoCacheMetrics _metrics;
+ private readonly AutoCacheOptions _options;
+
+ public AutoCacheManager(
+ IAutoCacheKeyManager autoCacheKeyManager,
+ ICurrentUser currentUser,
+ ILogger logger,
+ IAutoCacheMetrics metrics,
+ IOptions options)
+ {
+ _autoCacheKeyManager = autoCacheKeyManager;
+ _currentUser = currentUser;
+ _logger = logger;
+ _metrics = metrics;
+ _options = options.Value;
+ }
+
+ public async Task GetOrAddAsync(
+ object? caller,
+ Func> func,
+ object?[]? parameters = null,
+ Func? optionsFactory = null,
+ Type[]? invalidateOnEntities = null,
+ AutoCacheScope scope = AutoCacheScope.Global,
+ bool considerUow = false,
+ string? additionalCacheKey = null,
+ [CallerMemberName] string methodName = "")
+ {
+ if (!_options.Enabled)
+ {
+ return await func(); // 👈 Caching disabled, execute directly
+ }
+
+ var callerType = caller != null ? ProxyHelper.GetUnProxiedType(caller) : GetType();
+ parameters ??= [];
+
+ // Generate unique cache key based on method, parameters, and scope
+ var cacheKey = GenerateCacheKey(
+ callerType.Name,
+ additionalCacheKey,
+ methodName,
+ parameters,
+ scope);
+
+ var (cachedResult, exception, wasHit) = await GetOrAddCacheAsync(
+ cacheKey,
+ func,
+ optionsFactory,
+ considerUow
+ );
+
+ // Record metrics
+ if (wasHit)
+ {
+ _metrics.RecordHit(cacheKey);
+ }
+ else
+ {
+ _metrics.RecordMiss(cacheKey);
+ }
+
+ if (exception != null)
+ {
+ _metrics.RecordError(cacheKey, exception);
+
+ if (_options.ThrowOnError)
+ {
+ throw exception;
+ }
+ }
+
+ return cachedResult;
+ }
+
+ private string GenerateCacheKey(
+ string callerTypeName,
+ string? additionalCacheKey,
+ string methodName,
+ object?[] parameters,
+ AutoCacheScope scope)
+ {
+ var keyBuilder = new StringBuilder();
+ keyBuilder.Append($"{callerTypeName}:{methodName}");
+
+ // Add parameters to key
+ foreach (var param in parameters)
+ {
+ keyBuilder.Append($":{param}");
+ }
+
+ // Add scope-specific segments
+ if (scope.HasFlag(AutoCacheScope.CurrentUser) && _currentUser.Id.HasValue)
+ {
+ keyBuilder.Append($":user:{_currentUser.Id}"); // 👈 User-specific cache key
+ }
+
+ if (scope.HasFlag(AutoCacheScope.AuthenticatedUser))
+ {
+ keyBuilder.Append($":auth:{_currentUser.IsAuthenticated}");
+ }
+
+ if (!string.IsNullOrEmpty(additionalCacheKey))
+ {
+ keyBuilder.Append($":{additionalCacheKey}");
+ }
+
+ return keyBuilder.ToString();
+ }
+
+ // Additional methods for cache retrieval and storage...
+}
+```
+
+The manager generates unique cache keys based on method signatures, parameters, and scope settings.
+
+### Step - 6: Implement Cache Invalidation
+
+When entities change, related caches must be cleared. Create `AutoCacheInvalidationHandler.cs`:
+
+```csharp
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Volo.Abp.Domain.Entities;
+using Volo.Abp.Domain.Entities.Events;
+using Volo.Abp.EventBus;
+using Volo.Abp.Uow;
+
+namespace AutoCache;
+
+public class AutoCacheInvalidationHandler :
+ ILocalEventHandler>
+ where TEntity : class, IEntity
+{
+ private readonly IAutoCacheKeyManager _autoCacheKeyManager;
+ private readonly ILogger> _logger;
+ private readonly IUnitOfWorkManager _unitOfWorkManager;
+
+ public AutoCacheInvalidationHandler(
+ IAutoCacheKeyManager autoCacheKeyManager,
+ ILogger> logger,
+ IUnitOfWorkManager unitOfWorkManager)
+ {
+ _autoCacheKeyManager = autoCacheKeyManager;
+ _logger = logger;
+ _unitOfWorkManager = unitOfWorkManager;
+ }
+
+ public async Task HandleEventAsync(EntityChangedEventData eventData)
+ {
+ try
+ {
+ var entityType = typeof(TEntity);
+ var context = new RemoveCacheKeyContext
+ {
+ Keys = eventData.Entity.GetKeys()!
+ };
+
+ // Clear cache after unit of work completes
+ if(_unitOfWorkManager.Current != null)
+ {
+ _unitOfWorkManager.Current.OnCompleted(async () =>
+ {
+ await _autoCacheKeyManager.RemoveCacheAndCacheKeys(entityType, context); // 👈 Invalidate cache
+ });
+ }
+ else
+ {
+ await _autoCacheKeyManager.RemoveCacheAndCacheKeys(entityType, context);
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(
+ e,
+ "Error occurred while clearing cache for entity type {EntityType}",
+ typeof(TEntity).FullName
+ );
+ }
+ }
+}
+```
+
+
+
+This handler listens to entity change events and automatically clears related caches. The invalidation happens after the unit of work completes to ensure data consistency.
+
+### Step - 7: Configure AutoCache in Your Application
+
+Add the `AutoCacheModule` to your application module dependencies:
+
+```csharp
+[DependsOn(
+ typeof(AutoCacheModule), // 👈 Add AutoCache module
+ typeof(AbpCachingStackExchangeRedisModule),
+ // ... other modules
+)]
+public class YourApplicationModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.Enabled = true; // 👈 Enable caching
+ options.DefaultAbsoluteExpirationRelativeToNow = 3600000; // 1 hour
+ options.DefaultSlidingExpiration = 600000; // 10 minutes
+ options.ThrowOnError = false; // Fallback to method execution on cache errors
+ });
+
+ // Configure Redis (if using distributed cache)
+ Configure(options =>
+ {
+ options.KeyPrefix = "YourApp:";
+ });
+ }
+}
+```
+
+### Step - 8: Use Automatic Caching in Application Services
+
+Now comes the easy part - using automatic caching! Simply add the `[Cache]` attribute to your methods:
+
+```csharp
+using AutoCache;
+
+[Authorize(AutoCacheDemoPermissions.Books.Default)]
+public class BookAppService : ApplicationService, IBookAppService
+{
+ private readonly IRepository _repository;
+ private readonly AutoCacheManager _autoCacheManager;
+
+ public BookAppService(IRepository repository, AutoCacheManager autoCacheManager)
+ {
+ _repository = repository;
+ _autoCacheManager = autoCacheManager;
+ }
+
+ // Cache this method, invalidate when Book entity changes
+ [Cache(typeof(Book), Scope = AutoCacheScope.Global)]
+ public virtual async Task GetAsync(Guid id)
+ {
+ // You can also use AutoCacheManager directly for nested caching
+ var book = await _autoCacheManager.GetOrAddAsync(
+ this,
+ async () => await _repository.GetAsync(id),
+ [id], // 👈 Method parameters
+ invalidateOnEntities: [typeof(Book)],
+ scope: AutoCacheScope.Entity);
+
+ return ObjectMapper.Map(book!);
+ }
+
+ // Cache book list, invalidate when any Book changes
+ [Cache(typeof(Book))]
+ public virtual async Task> GetListAsync(PagedAndSortedResultRequestDto input)
+ {
+ var queryable = await _repository.GetQueryableAsync();
+ var query = queryable
+ .OrderBy(input.Sorting.IsNullOrWhiteSpace() ? "Name" : input.Sorting)
+ .Skip(input.SkipCount)
+ .Take(input.MaxResultCount);
+
+ var books = await AsyncExecuter.ToListAsync(query);
+ var totalCount = await AsyncExecuter.CountAsync(queryable);
+
+ return new PagedResultDto(
+ totalCount,
+ ObjectMapper.Map, List>(books)
+ );
+ }
+
+ // No caching on write operations
+ [Authorize(AutoCacheDemoPermissions.Books.Create)]
+ public async Task CreateAsync(CreateUpdateBookDto input)
+ {
+ var book = ObjectMapper.Map(input);
+ await _repository.InsertAsync(book); // 👈 This will trigger cache invalidation
+ return ObjectMapper.Map(book);
+ }
+}
+```
+
+**What Happens Here:**
+
+1. When `GetAsync` is called, the interceptor checks the cache
+2. On cache miss, the actual method executes and the result is cached
+3. When `CreateAsync` inserts a `Book`, the invalidation handler clears all caches related to `Book`
+4. Next call to `GetAsync` will fetch fresh data
+
+## Advanced Features
+
+### User-Specific Caching
+
+For user-specific data, use `AutoCacheScope.CurrentUser`:
+
+```csharp
+[Cache(typeof(Order), Scope = AutoCacheScope.CurrentUser)]
+public virtual async Task> GetMyOrdersAsync()
+{
+ var orders = await _orderRepository.GetListAsync(x => x.UserId == CurrentUser.Id);
+ return ObjectMapper.Map, List>(orders);
+}
+```
+
+Each user gets their own cache entry, automatically invalidated when their orders change.
+
+### Custom Cache Keys
+
+For fine-grained control, add custom cache key segments:
+
+```csharp
+[Cache(
+ typeof(Product),
+ Scope = AutoCacheScope.Global,
+ AdditionalCacheKey = "featured"
+)]
+public virtual async Task> GetFeaturedProductsAsync()
+{
+ // Only featured products are cached separately
+ return await GetProductsByCategoryAsync("Featured");
+}
+```
+
+### Performance Metrics
+
+Monitor cache performance using `IAutoCacheMetrics`:
+
+```csharp
+public class CacheMonitoringService : ITransientDependency
+{
+ private readonly IAutoCacheMetrics _metrics;
+
+ public CacheMonitoringService(IAutoCacheMetrics metrics)
+ {
+ _metrics = metrics;
+ }
+
+ public AutoCacheStatistics GetStatistics()
+ {
+ return _metrics.GetStatistics(); // 👈 Get hit rate, miss count, error count
+ }
+}
+```
+
+## Testing the Application
+
+### 1. Run the Application
+
+```bash
+abp new BookStore -u mvc -d ef
+cd BookStore
+dotnet run --project src/BookStore.Web
+```
+
+### 2. Test Cache Behavior
+
+Create a simple test to verify caching:
+
+```csharp
+[Fact]
+public async Task Should_Cache_Book_Results()
+{
+ // First call - cache miss
+ var book1 = await _bookAppService.GetAsync(testBookId);
+
+ // Second call - cache hit (should be faster)
+ var book2 = await _bookAppService.GetAsync(testBookId);
+
+ book1.Name.ShouldBe(book2.Name);
+}
+
+[Fact]
+public async Task Should_Invalidate_Cache_On_Update()
+{
+ // Cache the book
+ var book1 = await _bookAppService.GetAsync(testBookId);
+
+ // Update the book
+ await _bookAppService.UpdateAsync(testBookId, new CreateUpdateBookDto
+ {
+ Name = "Updated Name"
+ });
+
+ // Fetch again - should get updated data (cache was invalidated)
+ var book2 = await _bookAppService.GetAsync(testBookId);
+
+ book2.Name.ShouldBe("Updated Name");
+}
+```
+
+### 3. Monitor Cache Performance
+
+Check your application logs for cache metrics:
+
+```
+[INF] Cache Hit: BookAppService:GetAsync:book-id-123 (Response Time: 5ms)
+[INF] Cache Miss: BookAppService:GetListAsync (Response Time: 156ms)
+[INF] Cache Invalidation: Book entity changed, cleared 3 cache entries
+```
+
+## Key Takeaways
+
+✅ **Automatic caching reduces boilerplate code** - Just add `[Cache]` attribute to methods instead of manual cache management
+
+✅ **Smart invalidation keeps data fresh** - Entity changes automatically clear related caches without manual intervention
+
+✅ **Multiple scoping options** - Support for global, user-specific, authenticated, and entity-level caching strategies
+
+✅ **Built-in fallback handling** - Gracefully falls back to method execution if caching fails
+
+✅ **Performance monitoring** - Track cache hits, misses, and errors for optimization
+
+## Conclusion
+
+Automatic method-level caching dramatically simplifies performance optimization in ABP Framework applications. By using attributes and interceptors, you can add sophisticated caching behavior without cluttering your business logic with cache management code.
+
+The system we've built provides intelligent cache invalidation, multiple scoping strategies, and built-in monitoring - all while maintaining clean, readable code. Whether you're building a small application or an enterprise system, this approach scales elegantly and integrates seamlessly with ABP's architecture.
+
+Ready to implement this in your project? The complete working implementation is available in the [AbpAutoCacheDemo repository](https://github.com/salihozkara/AbpAutoCacheDemo). You can clone the repository, explore the code, and even extract the `src/AutoCache` folder to use it as a standalone library in your own ABP applications. The [main implementation commit](https://github.com/salihozkara/AbpAutoCacheDemo/commit/946df1fc07de6eddd26eb14013a09968cd59329b) shows all the components working together, including interceptor registration, cache key management, and automatic invalidation handlers.r you're building a small application or an enterprise system, this approach scales elegantly and integrates seamlessly with ABP's architecture.
+
+Ready to implement this in your project? Check out the complete working example in the repository linked below, and start improving your application's performance today!
+
+### See Also
+
+- [ABP Caching Documentation](https://abp.io/docs/latest/framework/fundamentals/caching)
+- [Interceptors in ABP](https://abp.io/docs/latest/framework/infrastructure/interceptors)
+- [Event Bus Documentation](https://abp.io/docs/latest/framework/infrastructure/event-bus)
+- [Sample Project on GitHub](https://github.com/salihozkara/AbpAutoCacheDemo)
+
+---
+
+## References
+
+- [ABP Framework Documentation](https://docs.abp.io)
+- [Redis Distributed Caching](https://redis.io/docs/)
+- [Aspect-Oriented Programming Patterns](https://en.wikipedia.org/wiki/Aspect-oriented_programming)
diff --git a/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/summary.md b/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/summary.md
new file mode 100644
index 0000000000..a27494cd8a
--- /dev/null
+++ b/docs/en/Community-Articles/2025-12-06-Implement-Automatic-Method-Level-Caching-in-ABP-Framework/summary.md
@@ -0,0 +1 @@
+Learn how to implement automatic method-level caching in ABP Framework using attributes and interceptors. This comprehensive guide covers building a reusable cache infrastructure with attribute-based caching, intelligent cache invalidation when entities change, support for multiple cache scopes (Global, CurrentUser, AuthenticatedUser, and Entity), seamless integration with ABP's dynamic proxy system and event bus, and built-in performance metrics for monitoring cache effectiveness in production applications.
diff --git a/docs/en/images/elsa-studio-wasm.png b/docs/en/images/elsa-studio-wasm.png
new file mode 100644
index 0000000000..9365ef6a4c
Binary files /dev/null and b/docs/en/images/elsa-studio-wasm.png differ
diff --git a/docs/en/modules/ai-management/index.md b/docs/en/modules/ai-management/index.md
index a126d9c59d..b9492d5a4f 100644
--- a/docs/en/modules/ai-management/index.md
+++ b/docs/en/modules/ai-management/index.md
@@ -10,8 +10,7 @@
> You must have an ABP Team or a higher license to use this module.
> **⚠️ Important Notice**
-> The **AI Management Module** is currently in **preview** and not yet production-ready. The documentation and implementation are subject to change.
-> We recommend using this module for **evaluation and experimentation** only, not in production environments for now.
+> The **AI Management Module** is currently in **preview**. The documentation and implementation are subject to change.
This module implements AI (Artificial Intelligence) management capabilities on top of the [Artificial Intelligence Workspaces](../../framework/infrastructure/artificial-intelligence/index.md) feature of the ABP Framework and allows to manage workspaces dynamically from the application including UI components and API endpoints.
@@ -137,7 +136,7 @@ PreConfigure(options =>
#### Dynamic Workspaces
-* **Created through the UI** or programmatically via `IWorkspaceRepository`
+* **Created through the UI** or programmatically via `ApplicationWorkspaceManager` and `IWorkspaceRepository`
* **Fully manageable** - can be created, updated, activated/deactivated, and deleted
* **Stored in database** with all configuration
* **Ideal for** user-customizable AI features
@@ -145,15 +144,30 @@ PreConfigure(options =>
Example (data seeding):
```csharp
-var workspace = new Workspace(
- name: "CustomerSupportWorkspace",
- provider: "OpenAI",
- modelName: "gpt-4",
- apiKey: "your-api-key"
-);
-workspace.ApplicationName = ApplicationInfoAccessor.ApplicationName;
-workspace.SystemPrompt = "You are a helpful customer support assistant.";
-await _workspaceRepository.InsertAsync(workspace);
+public class WorkspaceDataSeederContributor : IDataSeedContributor, ITransientDependency
+{
+ private readonly IWorkspaceRepository _workspaceRepository;
+ private readonly ApplicationWorkspaceManager _applicationWorkspaceManager;
+ public WorkspaceDataSeederContributor(
+ IWorkspaceRepository workspaceRepository,
+ ApplicationWorkspaceManager applicationWorkspaceManager)
+ {
+ _workspaceRepository = workspaceRepository;
+ _applicationWorkspaceManager = applicationWorkspaceManager;
+ }
+
+ public async Task SeedAsync(DataSeedContext context)
+ {
+ var workspace = await _applicationWorkspaceManager.CreateAsync(
+ name: "CustomerSupportWorkspace",
+ provider: "OpenAI",
+ modelName: "gpt-4");
+
+ workspace.ApiKey = "your-api-key";
+ workspace.SystemPrompt = "You are a helpful customer support assistant.";
+
+ await _workspaceRepository.InsertAsync(workspace);
+ }
```
### Workspace Naming Rules
@@ -179,12 +193,13 @@ The AI Management module defines the following permissions:
In addition to module-level permissions, you can restrict access to individual workspaces by setting the `RequiredPermissionName` property:
```csharp
-var workspace = new Workspace(
+var workspace = await _applicationWorkspaceManager.CreateAsync(
name: "PremiumWorkspace",
provider: "OpenAI",
- modelName: "gpt-4",
- requiredPermissionName: "MyApp.PremiumFeatures"
+ modelName: "gpt-4"
);
+// Set a specific permission for the workspace
+workspace.RequiredPermissionName = MyAppPermissions.AccessPremiumWorkspaces;
```
When a workspace has a required permission:
@@ -437,6 +452,37 @@ Your application acts as a proxy, forwarding these requests to the AI Management
| **3. Client Remote** | No | Remote Service | Remote Service | No | Microservices consuming AI centrally |
| **4. Client Proxy** | No | Remote Service | Remote Service | Yes | API Gateway pattern, proxy services |
+
+## Using Dynamic Workspace Configurations for custom requirements
+The AI Management module allows you to access only configuration of a workspace without resolving pre-constructed chat client. This is useful when you want to use a workspace for your own purposes and you don't need to use the chat client.
+The `IWorkspaceConfigurationStore` service is used to access the configuration of a workspace. It has multiple implementaations according to the usage scenario.
+
+```csharp
+public class MyService
+{
+ private readonly IWorkspaceConfigurationStore _workspaceConfigurationStore;
+ public MyService(IWorkspaceConfigurationStore workspaceConfigurationStore)
+ {
+ _workspaceConfigurationStore = workspaceConfigurationStore;
+ }
+
+ public async Task DoSomethingAsync()
+ {
+ // Get the configuration of the workspace that can be managed dynamically.
+ var configuration = await _workspaceConfigurationStore.GetAsync("MyWorkspace");
+
+ // Do something with the configuration
+ var kernel = Kernel.CreateBuilder()
+ .AddAzureOpenAIChatClient(
+ config.ModelName!,
+ new Uri(config.ApiBaseUrl),
+ config.ApiKey
+ )
+ .Build();
+ }
+}
+```
+
## Implementing Custom AI Provider Factories
While the AI Management module provides built-in support for OpenAI through the `Volo.AIManagement.OpenAI` package, you can easily add support for other AI providers by implementing a custom `IChatClientFactory`.
@@ -565,14 +611,14 @@ After implementing and registering your factory:
2. **Through Code** (data seeding):
```csharp
-await _workspaceRepository.InsertAsync(new Workspace(
- GuidGenerator.Create(),
- "MyOllamaWorkspace",
- provider: "Ollama",
- modelName: "mistral",
- apiBaseUrl: "http://localhost:11434",
- description: "Local Ollama workspace"
-));
+var workspace = await _applicationWorkspaceManager.CreateAsync(
+ name: "MyOllamaWorkspace",
+ provider: "Ollama",
+ modelName: "mistral"
+);
+workspace.ApiBaseUrl = "http://localhost:11434";
+workspace.Description = "Local Ollama workspace";
+await _workspaceRepository.InsertAsync(workspace);
```
> **Tip**: The provider name you use in `AddFactory("ProviderName")` must match the provider name stored in the workspace configuration in the database.
@@ -596,7 +642,7 @@ The following custom repositories are defined:
#### Domain Services
- `ApplicationWorkspaceManager`: Manages workspace operations and validations.
-- `WorkspaceConfigurationStore`: Retrieves workspace configuration with caching.
+- `WorkspaceConfigurationStore`: Retrieves workspace configuration with caching. Implements `IWorkspaceConfigurationStore` interface.
- `ChatClientResolver`: Resolves the appropriate `IChatClient` implementation for a workspace.
#### Integration Services
@@ -625,6 +671,9 @@ Workspace configurations are cached for performance. The cache key format:
WorkspaceConfiguration:{ApplicationName}:{WorkspaceName}
```
+### HttpApi Client Layer
+- `IntegrationWorkspaceConfigurationStore`: Integration service for remote workspace configuration retrieval. Implements `IWorkspaceConfigurationStore` interface.
+
The cache is automatically invalidated when workspaces are created, updated, or deleted.
## See Also
diff --git a/docs/en/modules/elsa-pro.md b/docs/en/modules/elsa-pro.md
index 3a6ec4533b..6b93e42368 100644
--- a/docs/en/modules/elsa-pro.md
+++ b/docs/en/modules/elsa-pro.md
@@ -1,10 +1,17 @@
+```json
+//[doc-seo]
+{
+ "Description": "Integrate Elsa Workflows into your ABP applications with this Pro module. Learn installation and setup for seamless workflow management."
+}
+```
+
# Elsa Module (Pro)
> You must have an ABP Team or a higher license to use this module.
This module integrates [Elsa Workflows](https://docs.elsaworkflows.io/) into ABP Framework applications and is designed to make it easy for developers to use Elsa's capabilities within their ABP-based projects. For creating, managing, and customizing workflows themselves, please refer to [the official Elsa documentation](https://docs.elsaworkflows.io/).
-## How to install
+## How to Install
The Elsa module is not installed in [the startup templates](../solution-templates/layered-web-application) by default and must be installed manually. There are two ways of installing a module into your application and each one of these approaches is explained in the next sections.
@@ -37,6 +44,23 @@ After adding the package references, open the module class of the project (e.g.:
> If you are using Blazor Web App, you need to add the `Volo.Elsa.Admin.Blazor.WebAssembly` package to the **{ProjectName}.Blazor.Client.csproj** project and add the `Volo.Elsa.Admin.Blazor.Server` package to the **{ProjectName}.Blazor.csproj** project.
+### `AbpElsaAspNetCoreModule` and `AbpElsaIdentityModule`
+
+These two modules generally will be added to your authentication project. Please add `Volo.Elsa.Abp.AspNetCore` and `Volo.Elsa.Abp.Identity` packages to your project and add the `AbpElsaAspNetCoreModule` and `AbpElsaIdentityModule` to the `DependsOn` attribute of your module class based on your project structure:
+
+```xml
+
+
+```
+
+```csharp
+[DependsOn(
+ //...
+ typeof(AbpElsaAspNetCoreModule),
+ typeof(AbpElsaIdentityModule)
+)]
+```
+
## The Elsa Module
The Elsa Workflows has its own database provider, and also has a Tenant/Role/User system. They are under active development, so the ABP Elsa module is not yet fully integrated. Below is the current status of each module in the ABP's Elsa Module:
@@ -56,6 +80,49 @@ The rest of the projects/modules are basically empty and will be implemented in
- `AbpElsaBlazorWebAssemblyModule(Volo.Elsa.Abp.Blazor.WebAssembly)`
- `AbpElsaWebModule(Volo.Elsa.Abp.Web)`
+## Configure the Elsa Server
+
+You need to configure Elsa in your ABP application to use its features. You can do that in the `ConfigureServices` method of your `YourElsaAppModule` class as shown below:
+
+> For more information about configuring Elsa, please refer to [the official Elsa documentation](https://docs.elsaworkflows.io/).
+
+```cs
+private void ConfigureElsa(ServiceConfigurationContext context, IConfiguration configuration)
+{
+ var connectionString = configuration.GetConnectionString("Default")!;
+ context.Services
+ .AddElsa(elsa => elsa
+ .UseAbpIdentity(identity => // Use UseAbpIdentity instead of UseIdentity to integrate with ABP Identity module
+ {
+ identity.TokenOptions = options => options.SigningKey = "large-signing-key-for-signing-JWT-tokens";
+ })
+ .UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlServer(connectionString)))
+ .UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(ef => ef.UseSqlServer(connectionString)))
+ .UseScheduling()
+ .UseJavaScript()
+ .UseLiquid()
+ .UseCSharp()
+ .UseHttp(http => http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options))
+ .UseWorkflowsApi()
+ .AddActivitiesFrom()
+ .AddWorkflowsFrom()
+ );
+}
+```
+
+## Elsa Database Migration
+
+Elsa module uses its own database context and migration system, ABP Elsa module doesn't contain any `aggregate root/entity` at the moment. So, **you don't need to create any initial migration for Elsa module**. You just need to configure the Elsa Services as follows:
+
+```cs
+.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlServer(connectionString)))
+.UseWorkflowRuntime(runtime => runtime.UseEntityFrameworkCore(ef => ef.UseSqlServer(connectionString)))
+```
+
+When you run your application, Elsa will create its own database tables if they do not exist.
+
+> See [how to configure Elsa Workflows to use different database providers for persistence, including SQL Server, PostgreSQL, and MongoDB](https://docs.elsaworkflows.io/getting-started/database-configuration) for more information.
+
### Elsa Module Permissions
The Elsa Workflow API endpoints check permissions. Also, it has a `*` wildcard permission to allow all permissions.
@@ -72,14 +139,24 @@ You can also grant parts of the permissions to a role or user. It will add the `
### Elsa Studio
-Elsa Studio is an **independent** web application that allows you to design, manage, and execute workflows. It is built using **Blazor Server/WebAssembly**.
+[Elsa Studio](https://docs.elsaworkflows.io/application-types/elsa-studio) is a **standalone** web application that allows you to design, manage, and execute workflows. It is built using **Blazor Server/WebAssembly**.
+
+`ElsaDemoApp.Studio.WASM` is a sample Blazor WebAssembly project that demonstrates how to use Elsa Studio with ELSA Server with ABP Framework.
+
+> Elsa Studio has its own layout and theme, and you can't integrate it into an ABP Blazor project for now.
+
+
+
+Please check the [Elsa Workflows - Sample Workflow Demo](../samples/elsa-workflows-demo.md) document to download its source code for review.
+
+#### Elsa Studio Authentication
Elsa Studio requires authentication and there are two ways to authenticate Elsa Studio:
* Password Flow Authentication
* Code Flow Authentication
-#### Elsa Studio - Password Flow Authentication
+##### Elsa Studio - Password Flow Authentication
The `AbpElsaIdentityModule(Volo.Elsa.Abp.Identity)` module is used to integrate with [ABP Identity module](./identity-pro.md) to check Elsa Studio *username* and *password* against ABP Identity.
@@ -109,7 +186,7 @@ Once, you logged in to the application, you can start defining workflows, manage

-#### Elsa Studio - Code Flow Authentication
+##### Elsa Studio - Code Flow Authentication
ABP applications use [OpenIddict](./openiddict-pro.md) for authentication. So, you can use the [Authorization Code Flow](https://oauth.net/2/grant-types/authorization-code/) to authenticate Elsa Studio.
diff --git a/docs/en/modules/identity-pro.md b/docs/en/modules/identity-pro.md
index 17c077c5c4..0d26154932 100644
--- a/docs/en/modules/identity-pro.md
+++ b/docs/en/modules/identity-pro.md
@@ -434,9 +434,10 @@ This module doesn't define any additional distributed event. See the [standard d
## See Also
* [Import External Users](./identity/import-external-users.md)
-* [LDAP Login](./identity/idap.md)
+* [LDAP Login](./identity/ldap.md)
* [OAuth Login](./identity/oauth-login.md)
* [Periodic Password Change (Password Aging)](./identity/periodic-password-change.md)
* [Two Factor Authentication](./identity/two-factor-authentication.md)
* [Session Management](./identity/session-management.md)
* [Password History](./identity/password-history.md)
+
diff --git a/docs/en/modules/identity/idap.md b/docs/en/modules/identity/ldap.md
similarity index 100%
rename from docs/en/modules/identity/idap.md
rename to docs/en/modules/identity/ldap.md
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryService.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryService.cs
index 2f684acd29..4f8f77b245 100644
--- a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryService.cs
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryService.cs
@@ -67,7 +67,7 @@ public class TelemetryService : ITelemetryService, IScopedDependency
private Task AddActivityAsync(ActivityContext context)
{
- _ = Task.Run(async () =>
+ var telemetryTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
@@ -81,6 +81,30 @@ public class TelemetryService : ITelemetryService, IScopedDependency
telemetryActivitySender);
});
+ AppDomain.CurrentDomain.ProcessExit += (_, _) =>
+ {
+ try
+ {
+ telemetryTask.Wait(TimeSpan.FromSeconds(10));
+ }
+ catch
+ {
+ // ignored
+ }
+ };
+
+ Console.CancelKeyPress += (_, _) =>
+ {
+ try
+ {
+ telemetryTask.Wait(TimeSpan.FromSeconds(10));
+ }
+ catch
+ {
+ // ignored
+ }
+ };
+
return Task.CompletedTask;
}
diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo.Abp.EntityFrameworkCore.Oracle.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo.Abp.EntityFrameworkCore.Oracle.csproj
index 7a4dbe61b1..8fd893c6c1 100644
--- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo.Abp.EntityFrameworkCore.Oracle.csproj
+++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo.Abp.EntityFrameworkCore.Oracle.csproj
@@ -22,8 +22,6 @@
-
-
diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo/Abp/EntityFrameworkCore/AbpSqliteOptions.cs b/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo/Abp/EntityFrameworkCore/AbpSqliteOptions.cs
new file mode 100644
index 0000000000..2d94b2864a
--- /dev/null
+++ b/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo/Abp/EntityFrameworkCore/AbpSqliteOptions.cs
@@ -0,0 +1,6 @@
+namespace Volo.Abp.EntityFrameworkCore;
+
+public class AbpSqliteOptions
+{
+ public int? BusyTimeout { get; set; }
+}
diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo/Abp/EntityFrameworkCore/Interceptors/SqliteBusyTimeoutSaveChangesInterceptor.cs b/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo/Abp/EntityFrameworkCore/Interceptors/SqliteBusyTimeoutSaveChangesInterceptor.cs
new file mode 100644
index 0000000000..32b4859bbb
--- /dev/null
+++ b/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo/Abp/EntityFrameworkCore/Interceptors/SqliteBusyTimeoutSaveChangesInterceptor.cs
@@ -0,0 +1,39 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+
+namespace Volo.Abp.EntityFrameworkCore.Interceptors;
+
+///
+/// https://github.com/dotnet/efcore/issues/29514
+///
+public class SqliteBusyTimeoutSaveChangesInterceptor : SaveChangesInterceptor
+{
+ private readonly string _pragmaCommand;
+
+ public SqliteBusyTimeoutSaveChangesInterceptor(int timeoutMilliseconds)
+ {
+ _pragmaCommand = $"PRAGMA busy_timeout={timeoutMilliseconds};";
+ }
+
+ public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result)
+ {
+ if (eventData.Context != null)
+ {
+ eventData.Context.Database.ExecuteSqlRaw(_pragmaCommand);
+ }
+
+ return result;
+ }
+
+ public override async ValueTask> SavingChangesAsync(DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default)
+ {
+ if (eventData.Context != null)
+ {
+ await eventData.Context.Database.ExecuteSqlRawAsync(_pragmaCommand, cancellationToken: cancellationToken);
+ }
+
+ return await base.SavingChangesAsync(eventData, result, cancellationToken);
+ }
+}
diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo/Abp/EntityFrameworkCore/Sqlite/AbpEntityFrameworkCoreSqliteModule.cs b/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo/Abp/EntityFrameworkCore/Sqlite/AbpEntityFrameworkCoreSqliteModule.cs
index 52415d73b1..6b767ceee4 100644
--- a/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo/Abp/EntityFrameworkCore/Sqlite/AbpEntityFrameworkCoreSqliteModule.cs
+++ b/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo/Abp/EntityFrameworkCore/Sqlite/AbpEntityFrameworkCoreSqliteModule.cs
@@ -1,4 +1,6 @@
+using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.EntityFrameworkCore.GlobalFilters;
+using Volo.Abp.EntityFrameworkCore.Interceptors;
using Volo.Abp.Modularity;
namespace Volo.Abp.EntityFrameworkCore.Sqlite;
@@ -8,11 +10,31 @@ namespace Volo.Abp.EntityFrameworkCore.Sqlite;
)]
public class AbpEntityFrameworkCoreSqliteModule : AbpModule
{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(options =>
+ {
+ options.BusyTimeout = 5000;
+ });
+ }
+
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure(options =>
{
options.UseDbFunction = true;
});
+
+ var sqliteOptions = context.Services.ExecutePreConfiguredActions();
+ if (sqliteOptions.BusyTimeout.HasValue)
+ {
+ Configure(options =>
+ {
+ options.ConfigureDefaultOnConfiguring((dbContext, dbContextOptionsBuilder) =>
+ {
+ dbContextOptionsBuilder.AddInterceptors(new SqliteBusyTimeoutSaveChangesInterceptor(sqliteOptions.BusyTimeout.Value));
+ }, overrideExisting: false);
+ });
+ }
}
}
diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContextOptions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContextOptions.cs
index a179bb3ee8..b87e9501d7 100644
--- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContextOptions.cs
+++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContextOptions.cs
@@ -26,11 +26,11 @@ public class AbpDbContextOptions
internal Dictionary> ConventionActions { get; }
internal Action? DefaultOnModelCreatingAction { get; set; }
-
- internal Action? DefaultOnConfiguringAction { get; set; }
internal Dictionary> OnModelCreatingActions { get; }
-
+
+ internal Action? DefaultOnConfiguringAction { get; set; }
+
internal Dictionary> OnConfiguringActions { get; }
public AbpDbContextOptions()
@@ -58,11 +58,18 @@ public class AbpDbContextOptions
DefaultConfigureAction = action;
}
- public void ConfigureDefaultConvention([NotNull] Action action)
+ public void ConfigureDefaultConvention([NotNull] Action action, bool overrideExisting = false)
{
Check.NotNull(action, nameof(action));
- DefaultConventionAction = action;
+ if (overrideExisting)
+ {
+ DefaultConventionAction = action;
+ }
+ else
+ {
+ DefaultConventionAction += action;
+ }
}
public void ConfigureConventions([NotNull] Action action)
@@ -83,18 +90,18 @@ public class AbpDbContextOptions
actions.Add(action);
}
- public void ConfigureDefaultOnModelCreating([NotNull] Action action)
- {
- Check.NotNull(action, nameof(action));
-
- DefaultOnModelCreatingAction = action;
- }
-
- public void ConfigureDefaultOnConfiguring([NotNull] Action action)
+ public void ConfigureDefaultOnModelCreating([NotNull] Action action, bool overrideExisting = false)
{
Check.NotNull(action, nameof(action));
- DefaultOnConfiguringAction = action;
+ if (overrideExisting)
+ {
+ DefaultOnModelCreatingAction = action;
+ }
+ else
+ {
+ DefaultOnModelCreatingAction += action;
+ }
}
public void ConfigureOnModelCreating([NotNull] Action action)
@@ -114,7 +121,21 @@ public class AbpDbContextOptions
actions.Add(action);
}
-
+
+ public void ConfigureDefaultOnConfiguring([NotNull] Action action, bool overrideExisting = false)
+ {
+ Check.NotNull(action, nameof(action));
+
+ if (overrideExisting)
+ {
+ DefaultOnConfiguringAction = action;
+ }
+ else
+ {
+ DefaultOnConfiguringAction += action;
+ }
+ }
+
public void ConfigureOnConfiguring([NotNull] Action action)
where TDbContext : AbpDbContext
{
diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs
index 4bdf6a2782..a3376d9429 100644
--- a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs
+++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs
@@ -168,7 +168,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen
GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear());
}
- protected async override Task PublishToEventBusAsync(Type eventType, object eventData)
+ protected override async Task PublishToEventBusAsync(Type eventType, object eventData)
{
var headers = new Headers
{
@@ -193,7 +193,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen
unitOfWork.AddOrReplaceDistributedEvent(eventRecord);
}
- public async override Task PublishFromOutboxAsync(
+ public override async Task PublishFromOutboxAsync(
OutgoingEventInfo outgoingEvent,
OutboxConfig outboxConfig)
{
@@ -206,13 +206,18 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen
headers.Add(EventBusConsts.CorrelationIdHeaderName, System.Text.Encoding.UTF8.GetBytes(outgoingEvent.GetCorrelationId()!));
}
- await PublishAsync(
+ var result = await PublishAsync(
AbpKafkaEventBusOptions.TopicName,
outgoingEvent.EventName,
outgoingEvent.EventData,
headers
);
+ if (result.Status != PersistenceStatus.Persisted)
+ {
+ throw new AbpException($"Failed to publish event '{outgoingEvent.EventName}' to topic '{AbpKafkaEventBusOptions.TopicName}'. Status: {result.Status}");
+ }
+
using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId()))
{
await TriggerDistributedEventSentAsync(new DistributedEventSent()
@@ -224,7 +229,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen
}
}
- public async override Task PublishManyFromOutboxAsync(IEnumerable outgoingEvents, OutboxConfig outboxConfig)
+ public override async Task PublishManyFromOutboxAsync(IEnumerable outgoingEvents, OutboxConfig outboxConfig)
{
var producer = ProducerPool.Get(AbpKafkaEventBusOptions.ConnectionName);
var outgoingEventArray = outgoingEvents.ToArray();
@@ -242,7 +247,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen
headers.Add(EventBusConsts.CorrelationIdHeaderName, System.Text.Encoding.UTF8.GetBytes(outgoingEvent.GetCorrelationId()!));
}
- producer.Produce(
+ var result = await producer.ProduceAsync(
AbpKafkaEventBusOptions.TopicName,
new Message
{
@@ -251,6 +256,11 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen
Headers = headers
});
+ if (result.Status != PersistenceStatus.Persisted)
+ {
+ throw new AbpException($"Failed to publish event '{outgoingEvent.EventName}' to topic '{AbpKafkaEventBusOptions.TopicName}'. Status: {result.Status}");
+ }
+
using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId()))
{
await TriggerDistributedEventSentAsync(new DistributedEventSent()
@@ -263,7 +273,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen
}
}
- public async override Task ProcessFromInboxAsync(
+ public override async Task ProcessFromInboxAsync(
IncomingEventInfo incomingEvent,
InboxConfig inboxConfig)
{
@@ -290,12 +300,16 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen
return Serializer.Serialize(eventData);
}
- private Task PublishAsync(string topicName, Type eventType, object eventData, Headers headers)
+ private async Task PublishAsync(string topicName, Type eventType, object eventData, Headers headers)
{
var eventName = EventNameAttribute.GetNameOrDefault(eventType);
var body = Serializer.Serialize(eventData);
- return PublishAsync(topicName, eventName, body, headers);
+ var result = await PublishAsync(topicName, eventName, body, headers);
+ if (result.Status != PersistenceStatus.Persisted)
+ {
+ throw new AbpException($"Failed to publish event '{eventName}' to topic '{topicName}'. Status: {result.Status}");
+ }
}
private Task> PublishAsync(
diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs
index 23b9b71a57..a22f5970a2 100644
--- a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs
+++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs
@@ -17,7 +17,7 @@ public class ProducerPool : IProducerPool, ISingletonDependency
protected ConcurrentDictionary>> Producers { get; }
protected TimeSpan TotalDisposeWaitDuration { get; set; } = TimeSpan.FromSeconds(10);
-
+
protected TimeSpan DefaultTransactionsWaitDuration { get; set; } = TimeSpan.FromSeconds(30);
public ILogger Logger { get; set; }
@@ -41,8 +41,10 @@ public class ProducerPool : IProducerPool, ISingletonDependency
{
var producerConfig = new ProducerConfig(Options.Connections.GetOrDefault(connection).ToDictionary(k => k.Key, v => v.Value));
Options.ConfigureProducer?.Invoke(producerConfig);
+ producerConfig.Acks ??= Acks.All;
+ producerConfig.EnableIdempotence ??= true;
return new ProducerBuilder(producerConfig).Build();
-
+
})).Value;
}
@@ -70,7 +72,7 @@ public class ProducerPool : IProducerPool, ISingletonDependency
foreach (var producer in Producers.Values)
{
var poolItemDisposeStopwatch = Stopwatch.StartNew();
-
+
try
{
producer.Value.Dispose();
@@ -78,19 +80,19 @@ public class ProducerPool : IProducerPool, ISingletonDependency
catch
{
}
-
+
poolItemDisposeStopwatch.Stop();
-
+
remainingWaitDuration = remainingWaitDuration > poolItemDisposeStopwatch.Elapsed
? remainingWaitDuration.Subtract(poolItemDisposeStopwatch.Elapsed)
: TimeSpan.Zero;
}
-
+
poolDisposeStopwatch.Stop();
-
+
Logger.LogInformation(
$"Disposed Kafka Producer Pool ({Producers.Count} producers in {poolDisposeStopwatch.Elapsed.TotalMilliseconds:0.00} ms).");
-
+
if (poolDisposeStopwatch.Elapsed.TotalSeconds > 5.0)
{
Logger.LogWarning(
diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs
index 4351ef62bb..602540dd03 100644
--- a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs
+++ b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs
@@ -1,6 +1,8 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
@@ -8,25 +10,31 @@ namespace Volo.Abp.MultiTenancy;
public class TenantResolver : ITenantResolver, ITransientDependency
{
- private readonly IServiceProvider _serviceProvider;
- private readonly AbpTenantResolveOptions _options;
+ public ILogger Logger { get; set; }
+
+ protected IServiceProvider ServiceProvider { get; }
+ protected AbpTenantResolveOptions Options { get; }
public TenantResolver(IOptions options, IServiceProvider serviceProvider)
{
- _serviceProvider = serviceProvider;
- _options = options.Value;
+ Logger = NullLogger.Instance;
+
+ ServiceProvider = serviceProvider;
+ Options = options.Value;
}
public virtual async Task ResolveTenantIdOrNameAsync()
{
var result = new TenantResolveResult();
- using (var serviceScope = _serviceProvider.CreateScope())
+ Logger.LogDebug("Starting resolving tenant...");
+ using (var serviceScope = ServiceProvider.CreateScope())
{
var context = new TenantResolveContext(serviceScope.ServiceProvider);
- foreach (var tenantResolver in _options.TenantResolvers)
+ foreach (var tenantResolver in Options.TenantResolvers)
{
+ Logger.LogDebug("Trying to resolve tenant through '{TenantResolverName}'...", tenantResolver.Name);
await tenantResolver.ResolveAsync(context);
result.AppliedResolvers.Add(tenantResolver.Name);
@@ -34,15 +42,21 @@ public class TenantResolver : ITenantResolver, ITransientDependency
if (context.HasResolvedTenantOrHost())
{
result.TenantIdOrName = context.TenantIdOrName;
+ Logger.LogDebug("Tenant resolved by '{TenantResolverName}' as '{TenantIdOrName}'.", tenantResolver.Name, result.TenantIdOrName ?? "Host");
break;
}
}
}
- if (result.TenantIdOrName.IsNullOrEmpty() && !string.IsNullOrWhiteSpace(_options.FallbackTenant))
+ if (result.TenantIdOrName.IsNullOrEmpty() && !string.IsNullOrWhiteSpace(Options.FallbackTenant))
{
- result.TenantIdOrName = _options.FallbackTenant;
+ result.TenantIdOrName = Options.FallbackTenant;
result.AppliedResolvers.Add(TenantResolverNames.FallbackTenant);
+ Logger.LogDebug("No tenant resolved. Using fallback tenant as '{FallbackTenant}'.", result.TenantIdOrName);
+ }
+ else if (result.TenantIdOrName.IsNullOrEmpty())
+ {
+ Logger.LogDebug("No tenant resolved.");
}
return result;
diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs
index 5f905ca062..a7b5c1e602 100644
--- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs
+++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs
@@ -30,6 +30,7 @@ public class AbpEntityFrameworkCoreTestModule : AbpModule
public override void PreConfigureServices(ServiceConfigurationContext context)
{
TestEntityExtensionConfigurator.Configure();
+ PreConfigure(x => x.BusyTimeout = null);
}
public override void ConfigureServices(ServiceConfigurationContext context)
diff --git a/latest-versions.json b/latest-versions.json
index 56a156bb81..59f9c65cf3 100644
--- a/latest-versions.json
+++ b/latest-versions.json
@@ -1,4 +1,13 @@
[
+ {
+ "version": "9.3.7",
+ "releaseDate": "",
+ "type": "stable",
+ "message": "",
+ "leptonx": {
+ "version": "4.3.7"
+ }
+ },
{
"version": "10.0.1",
"releaseDate": "",
diff --git a/modules/audit-logging/test/Volo.Abp.AuditLogging.EntityFrameworkCore.Tests/Volo/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingEntityFrameworkCoreTestModule.cs b/modules/audit-logging/test/Volo.Abp.AuditLogging.EntityFrameworkCore.Tests/Volo/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingEntityFrameworkCoreTestModule.cs
index 42d246625e..9d73151632 100644
--- a/modules/audit-logging/test/Volo.Abp.AuditLogging.EntityFrameworkCore.Tests/Volo/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingEntityFrameworkCoreTestModule.cs
+++ b/modules/audit-logging/test/Volo.Abp.AuditLogging.EntityFrameworkCore.Tests/Volo/Abp/AuditLogging/EntityFrameworkCore/AbpAuditLoggingEntityFrameworkCoreTestModule.cs
@@ -16,6 +16,11 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore;
)]
public class AbpAuditLoggingEntityFrameworkCoreTestModule : AbpModule
{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(x => x.BusyTimeout = null);
+ }
+
public override void ConfigureServices(ServiceConfigurationContext context)
{
var sqliteConnection = CreateDatabaseAndGetConnection();
diff --git a/modules/background-jobs/test/Volo.Abp.BackgroundJobs.EntityFrameworkCore.Tests/Volo/Abp/BackgroundJobs/EntityFrameworkCore/AbpBackgroundJobsEntityFrameworkCoreTestModule.cs b/modules/background-jobs/test/Volo.Abp.BackgroundJobs.EntityFrameworkCore.Tests/Volo/Abp/BackgroundJobs/EntityFrameworkCore/AbpBackgroundJobsEntityFrameworkCoreTestModule.cs
index c19016d0d0..ece6052449 100644
--- a/modules/background-jobs/test/Volo.Abp.BackgroundJobs.EntityFrameworkCore.Tests/Volo/Abp/BackgroundJobs/EntityFrameworkCore/AbpBackgroundJobsEntityFrameworkCoreTestModule.cs
+++ b/modules/background-jobs/test/Volo.Abp.BackgroundJobs.EntityFrameworkCore.Tests/Volo/Abp/BackgroundJobs/EntityFrameworkCore/AbpBackgroundJobsEntityFrameworkCoreTestModule.cs
@@ -16,6 +16,11 @@ namespace Volo.Abp.BackgroundJobs.EntityFrameworkCore;
)]
public class AbpBackgroundJobsEntityFrameworkCoreTestModule : AbpModule
{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(x => x.BusyTimeout = null);
+ }
+
public override void ConfigureServices(ServiceConfigurationContext context)
{
var sqliteConnection = CreateDatabaseAndGetConnection();
diff --git a/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests/EntityFrameworkCore/BlobStoringDatabaseEntityFrameworkCoreTestModule.cs b/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests/EntityFrameworkCore/BlobStoringDatabaseEntityFrameworkCoreTestModule.cs
index 038ca5874d..beae51969a 100644
--- a/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests/EntityFrameworkCore/BlobStoringDatabaseEntityFrameworkCoreTestModule.cs
+++ b/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests/EntityFrameworkCore/BlobStoringDatabaseEntityFrameworkCoreTestModule.cs
@@ -15,6 +15,11 @@ namespace Volo.Abp.BlobStoring.Database.EntityFrameworkCore;
)]
public class BlobStoringDatabaseEntityFrameworkCoreTestModule : AbpModule
{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(x => x.BusyTimeout = null);
+ }
+
public override void ConfigureServices(ServiceConfigurationContext context)
{
var sqliteConnection = CreateDatabaseAndGetConnection();
diff --git a/modules/blogging/test/Volo.Blogging.EntityFrameworkCore.Tests/Volo/Blogging/EntityFrameworkCore/BloggingEntityFrameworkCoreTestModule.cs b/modules/blogging/test/Volo.Blogging.EntityFrameworkCore.Tests/Volo/Blogging/EntityFrameworkCore/BloggingEntityFrameworkCoreTestModule.cs
index 498e8e7ab6..2d40880875 100644
--- a/modules/blogging/test/Volo.Blogging.EntityFrameworkCore.Tests/Volo/Blogging/EntityFrameworkCore/BloggingEntityFrameworkCoreTestModule.cs
+++ b/modules/blogging/test/Volo.Blogging.EntityFrameworkCore.Tests/Volo/Blogging/EntityFrameworkCore/BloggingEntityFrameworkCoreTestModule.cs
@@ -18,6 +18,11 @@ namespace Volo.Blogging.EntityFrameworkCore
{
private SqliteConnection _sqliteConnection;
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(x => x.BusyTimeout = null);
+ }
+
public override void ConfigureServices(ServiceConfigurationContext context)
{
_sqliteConnection = CreateDatabaseAndGetConnection();
diff --git a/modules/cms-kit/test/Volo.CmsKit.EntityFrameworkCore.Tests/EntityFrameworkCore/CmsKitEntityFrameworkCoreTestModule.cs b/modules/cms-kit/test/Volo.CmsKit.EntityFrameworkCore.Tests/EntityFrameworkCore/CmsKitEntityFrameworkCoreTestModule.cs
index 3c28251536..659fa551b2 100644
--- a/modules/cms-kit/test/Volo.CmsKit.EntityFrameworkCore.Tests/EntityFrameworkCore/CmsKitEntityFrameworkCoreTestModule.cs
+++ b/modules/cms-kit/test/Volo.CmsKit.EntityFrameworkCore.Tests/EntityFrameworkCore/CmsKitEntityFrameworkCoreTestModule.cs
@@ -20,6 +20,7 @@ public class CmsKitEntityFrameworkCoreTestModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
+ PreConfigure(x => x.BusyTimeout = null);
context.Services.AddDataMigrationEnvironment();
}
diff --git a/modules/docs/test/Volo.Docs.EntityFrameworkCore.Tests/Volo/Docs/EntityFrameworkCore/DocsEntityFrameworkCoreTestModule.cs b/modules/docs/test/Volo.Docs.EntityFrameworkCore.Tests/Volo/Docs/EntityFrameworkCore/DocsEntityFrameworkCoreTestModule.cs
index fe3e32291f..b61b8368d6 100644
--- a/modules/docs/test/Volo.Docs.EntityFrameworkCore.Tests/Volo/Docs/EntityFrameworkCore/DocsEntityFrameworkCoreTestModule.cs
+++ b/modules/docs/test/Volo.Docs.EntityFrameworkCore.Tests/Volo/Docs/EntityFrameworkCore/DocsEntityFrameworkCoreTestModule.cs
@@ -15,6 +15,11 @@ namespace Volo.Docs.EntityFrameworkCore
)]
public class DocsEntityFrameworkCoreTestModule : AbpModule
{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(x => x.BusyTimeout = null);
+ }
+
public override void ConfigureServices(ServiceConfigurationContext context)
{
var sqliteConnection = CreateDatabaseAndGetConnection();
diff --git a/modules/feature-management/test/Volo.Abp.FeatureManagement.EntityFrameworkCore.Tests/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreTestModule.cs b/modules/feature-management/test/Volo.Abp.FeatureManagement.EntityFrameworkCore.Tests/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreTestModule.cs
index 242cf75233..c370e5f558 100644
--- a/modules/feature-management/test/Volo.Abp.FeatureManagement.EntityFrameworkCore.Tests/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreTestModule.cs
+++ b/modules/feature-management/test/Volo.Abp.FeatureManagement.EntityFrameworkCore.Tests/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreTestModule.cs
@@ -19,6 +19,11 @@ namespace Volo.Abp.FeatureManagement.EntityFrameworkCore;
)]
public class AbpFeatureManagementEntityFrameworkCoreTestModule : AbpModule
{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(x => x.BusyTimeout = null);
+ }
+
public override void ConfigureServices(ServiceConfigurationContext context)
{
var sqliteConnection = CreateDatabaseAndGetConnection();
diff --git a/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentityEntityFrameworkCoreTestModule.cs b/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentityEntityFrameworkCoreTestModule.cs
index f0d1966bd0..6ee1dd34b7 100644
--- a/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentityEntityFrameworkCoreTestModule.cs
+++ b/modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentityEntityFrameworkCoreTestModule.cs
@@ -18,6 +18,11 @@ namespace Volo.Abp.Identity.EntityFrameworkCore;
)]
public class AbpIdentityEntityFrameworkCoreTestModule : AbpModule
{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(x => x.BusyTimeout = null);
+ }
+
public override void ConfigureServices(ServiceConfigurationContext context)
{
var sqliteConnection = CreateDatabaseAndGetConnection();
diff --git a/modules/openiddict/test/Volo.Abp.OpenIddict.EntityFrameworkCore.Tests/Volo/Abp/OpenIddict/EntityFrameworkCore/OpenIddictEntityFrameworkCoreTestModule.cs b/modules/openiddict/test/Volo.Abp.OpenIddict.EntityFrameworkCore.Tests/Volo/Abp/OpenIddict/EntityFrameworkCore/OpenIddictEntityFrameworkCoreTestModule.cs
index c47ecc7996..d2286bacbb 100644
--- a/modules/openiddict/test/Volo.Abp.OpenIddict.EntityFrameworkCore.Tests/Volo/Abp/OpenIddict/EntityFrameworkCore/OpenIddictEntityFrameworkCoreTestModule.cs
+++ b/modules/openiddict/test/Volo.Abp.OpenIddict.EntityFrameworkCore.Tests/Volo/Abp/OpenIddict/EntityFrameworkCore/OpenIddictEntityFrameworkCoreTestModule.cs
@@ -20,6 +20,11 @@ namespace Volo.Abp.OpenIddict.EntityFrameworkCore;
)]
public class OpenIddictEntityFrameworkCoreTestModule : AbpModule
{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(x => x.BusyTimeout = null);
+ }
+
public override void ConfigureServices(ServiceConfigurationContext context)
{
var sqliteConnection = CreateDatabaseAndGetConnection();
diff --git a/modules/permission-management/test/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests.csproj b/modules/permission-management/test/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests.csproj
index f99bf791dd..45c157641b 100644
--- a/modules/permission-management/test/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests.csproj
+++ b/modules/permission-management/test/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests.csproj
@@ -15,10 +15,10 @@
+
-
diff --git a/modules/permission-management/test/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests/Volo/Abp/PermissionManagement/EntityFrameworkCore/AbpPermissionManagementEntityFrameworkCoreTestModule.cs b/modules/permission-management/test/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests/Volo/Abp/PermissionManagement/EntityFrameworkCore/AbpPermissionManagementEntityFrameworkCoreTestModule.cs
index a73e5afb86..1e483fe1aa 100644
--- a/modules/permission-management/test/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests/Volo/Abp/PermissionManagement/EntityFrameworkCore/AbpPermissionManagementEntityFrameworkCoreTestModule.cs
+++ b/modules/permission-management/test/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests/Volo/Abp/PermissionManagement/EntityFrameworkCore/AbpPermissionManagementEntityFrameworkCoreTestModule.cs
@@ -1,36 +1,62 @@
-using System;
-using System.Threading.Tasks;
+using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.EntityFrameworkCore;
+using Volo.Abp.EntityFrameworkCore.Sqlite;
using Volo.Abp.Modularity;
using Volo.Abp.Threading;
using Volo.Abp.Uow;
+using Microsoft.Data.Sqlite;
namespace Volo.Abp.PermissionManagement.EntityFrameworkCore;
[DependsOn(
typeof(AbpPermissionManagementEntityFrameworkCoreModule),
- typeof(AbpPermissionManagementTestBaseModule))]
+ typeof(AbpPermissionManagementTestBaseModule),
+ typeof(AbpEntityFrameworkCoreSqliteModule)
+)]
public class AbpPermissionManagementEntityFrameworkCoreTestModule : AbpModule
{
- public override void ConfigureServices(ServiceConfigurationContext context)
+ public override void PreConfigureServices(ServiceConfigurationContext context)
{
- context.Services.AddEntityFrameworkInMemoryDatabase();
+ PreConfigure(x => x.BusyTimeout = null);
+ }
- var databaseName = Guid.NewGuid().ToString();
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ var sqliteConnection = CreateDatabaseAndGetConnection();
Configure(options =>
{
options.Configure(abpDbContextConfigurationContext =>
{
- abpDbContextConfigurationContext.DbContextOptions.UseInMemoryDatabase(databaseName);
+ abpDbContextConfigurationContext.DbContextOptions.UseSqlite(sqliteConnection);
});
});
+ Configure(options =>
+ {
+ options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled; //EF in-memory database does not support transactions
+ });
+
context.Services.AddAlwaysDisableUnitOfWorkTransaction();
}
+ private static SqliteConnection CreateDatabaseAndGetConnection()
+ {
+ var connection = new AbpUnitTestSqliteConnection("Data Source=:memory:");
+ connection.Open();
+
+ new PermissionManagementDbContext(
+ new DbContextOptionsBuilder().UseSqlite(connection).Options
+ ).GetService().CreateTables();
+
+ return connection;
+ }
+
+
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var task = context.ServiceProvider.GetRequiredService().GetInitializeDynamicPermissionsTask();
diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Installer/AngularInstallationInfo.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Installer/AngularInstallationInfo.json
index ae67780dee..59abcbddf8 100644
--- a/modules/setting-management/src/Volo.Abp.SettingManagement.Installer/AngularInstallationInfo.json
+++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Installer/AngularInstallationInfo.json
@@ -2,6 +2,7 @@
"packages":[
{
"name": "@abp/ng.setting-management",
+ "keepPackageInPackageJson": true,
"appRoutingModuleConfiguration":{
"routes":[
"{ path: 'setting-management', loadChildren: () => import('@abp/ng.setting-management').then(c => c.createRoutes()),}"
diff --git a/modules/setting-management/test/Volo.Abp.SettingManagement.EntityFrameworkCore.Tests/Volo/Abp/SettingManagement/EntityFrameworkCore/AbpSettingManagementEntityFrameworkCoreTestModule.cs b/modules/setting-management/test/Volo.Abp.SettingManagement.EntityFrameworkCore.Tests/Volo/Abp/SettingManagement/EntityFrameworkCore/AbpSettingManagementEntityFrameworkCoreTestModule.cs
index 58df348af7..c5249ce8f0 100644
--- a/modules/setting-management/test/Volo.Abp.SettingManagement.EntityFrameworkCore.Tests/Volo/Abp/SettingManagement/EntityFrameworkCore/AbpSettingManagementEntityFrameworkCoreTestModule.cs
+++ b/modules/setting-management/test/Volo.Abp.SettingManagement.EntityFrameworkCore.Tests/Volo/Abp/SettingManagement/EntityFrameworkCore/AbpSettingManagementEntityFrameworkCoreTestModule.cs
@@ -17,6 +17,11 @@ namespace Volo.Abp.SettingManagement.EntityFrameworkCore;
)]
public class AbpSettingManagementEntityFrameworkCoreTestModule : AbpModule
{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(x => x.BusyTimeout = null);
+ }
+
public override void ConfigureServices(ServiceConfigurationContext context)
{
var sqliteConnection = CreateDatabaseAndGetConnection();
diff --git a/modules/tenant-management/test/Volo.Abp.TenantManagement.EntityFrameworkCore.Tests/Volo/Abp/TenantManagement/EntityFrameworkCore/AbpTenantManagementEntityFrameworkCoreTestModule.cs b/modules/tenant-management/test/Volo.Abp.TenantManagement.EntityFrameworkCore.Tests/Volo/Abp/TenantManagement/EntityFrameworkCore/AbpTenantManagementEntityFrameworkCoreTestModule.cs
index 336fbb228f..4b058822a9 100644
--- a/modules/tenant-management/test/Volo.Abp.TenantManagement.EntityFrameworkCore.Tests/Volo/Abp/TenantManagement/EntityFrameworkCore/AbpTenantManagementEntityFrameworkCoreTestModule.cs
+++ b/modules/tenant-management/test/Volo.Abp.TenantManagement.EntityFrameworkCore.Tests/Volo/Abp/TenantManagement/EntityFrameworkCore/AbpTenantManagementEntityFrameworkCoreTestModule.cs
@@ -17,6 +17,11 @@ namespace Volo.Abp.TenantManagement.EntityFrameworkCore;
)]
public class AbpTenantManagementEntityFrameworkCoreTestModule : AbpModule
{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(x => x.BusyTimeout = null);
+ }
+
public override void ConfigureServices(ServiceConfigurationContext context)
{
var sqliteConnection = CreateDatabaseAndGetConnection();
diff --git a/source-code/Volo.Abp.BasicTheme.SourceCode/Volo.Abp.BasicTheme.SourceCode.zip b/source-code/Volo.Abp.BasicTheme.SourceCode/Volo.Abp.BasicTheme.SourceCode.zip
index 8d59ce6a20..6bb97f976a 100644
Binary files a/source-code/Volo.Abp.BasicTheme.SourceCode/Volo.Abp.BasicTheme.SourceCode.zip and b/source-code/Volo.Abp.BasicTheme.SourceCode/Volo.Abp.BasicTheme.SourceCode.zip differ
diff --git a/source-code/Volo.ClientSimulation.SourceCode/Volo.ClientSimulation.SourceCode.zip b/source-code/Volo.ClientSimulation.SourceCode/Volo.ClientSimulation.SourceCode.zip
index 3852bd5bb6..3514451835 100644
Binary files a/source-code/Volo.ClientSimulation.SourceCode/Volo.ClientSimulation.SourceCode.zip and b/source-code/Volo.ClientSimulation.SourceCode/Volo.ClientSimulation.SourceCode.zip differ