From 1be22edac9c27bcd3529910ad3ea7cc9d0fda0e9 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 24 Nov 2025 15:37:21 +0800 Subject: [PATCH] Introduce AddAbpOptions to prevent options deadlocks Related to #24247 --- .../AbpAspNetCoreMvcNewtonsoftModule.cs | 2 +- .../AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs | 2 +- .../Mvc/Json/MvcCoreBuilderExtensions.cs | 2 +- .../ServiceCollectionOptionsExtensions.cs | 22 ++++++++++++ .../Abp/Options/AbpUnnamedOptionsManager.cs | 34 +++++++++++++++++++ .../Newtonsoft/AbpJsonNewtonsoftModule.cs | 2 +- .../AbpJsonSystemTextJsonModule.cs | 2 +- .../Volo/Abp/Json/AbpJsonTestModule.cs | 4 +-- .../Abp/MemoryDb/AbpMemoryDbTestModule.cs | 2 +- .../AspNetCore/AbpIdentityAspNetCoreModule.cs | 2 +- 10 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceCollectionOptionsExtensions.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Options/AbpUnnamedOptionsManager.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.NewtonsoftJson/Volo/Abp/AspNetCore/Mvc/NewtonsoftJson/AbpAspNetCoreMvcNewtonsoftModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.NewtonsoftJson/Volo/Abp/AspNetCore/Mvc/NewtonsoftJson/AbpAspNetCoreMvcNewtonsoftModule.cs index 95e07a06e0..214b866197 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.NewtonsoftJson/Volo/Abp/AspNetCore/Mvc/NewtonsoftJson/AbpAspNetCoreMvcNewtonsoftModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.NewtonsoftJson/Volo/Abp/AspNetCore/Mvc/NewtonsoftJson/AbpAspNetCoreMvcNewtonsoftModule.cs @@ -13,7 +13,7 @@ public class AbpAspNetCoreMvcNewtonsoftModule : AbpModule { context.Services.AddMvcCore().AddNewtonsoftJson(); - context.Services.AddOptions() + context.Services.AddAbpOptions() .Configure((options, rootServiceProvider) => { options.SerializerSettings.ContractResolver = diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs index c3fc470ed8..ad8d8c1c28 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs @@ -165,7 +165,7 @@ public class AbpAspNetCoreMvcModule : AbpModule context.Services.AddSingleton(); context.Services.TryAddEnumerable(ServiceDescriptor.Transient()); - context.Services.AddOptions() + context.Services.AddAbpOptions() .Configure((mvcOptions, serviceProvider) => { mvcOptions.AddAbp(context.Services); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Json/MvcCoreBuilderExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Json/MvcCoreBuilderExtensions.cs index f2bac81316..89af08fd43 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Json/MvcCoreBuilderExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Json/MvcCoreBuilderExtensions.cs @@ -13,7 +13,7 @@ public static class MvcCoreBuilderExtensions { public static IMvcCoreBuilder AddAbpJson(this IMvcCoreBuilder builder) { - builder.Services.AddOptions() + builder.Services.AddAbpOptions() .Configure((options, rootServiceProvider) => { options.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip; diff --git a/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceCollectionOptionsExtensions.cs b/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceCollectionOptionsExtensions.cs new file mode 100644 index 0000000000..43230c4dbb --- /dev/null +++ b/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceCollectionOptionsExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Volo.Abp.Options; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceCollectionOptionsExtensions +{ + /// + /// You should only use this method to register options if you need to continue using the ServiceProvider to get other options in your Options configuration method. + /// Otherwise, please use the default AddOptions method for better performance. + /// + /// + /// + /// + public static OptionsBuilder AddAbpOptions(this IServiceCollection services) + where TOptions : class + { + services.TryAddSingleton, AbpUnnamedOptionsManager>(); + return services.AddOptions(); + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Options/AbpUnnamedOptionsManager.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Options/AbpUnnamedOptionsManager.cs new file mode 100644 index 0000000000..eeb46271ee --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Options/AbpUnnamedOptionsManager.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Options; + +namespace Volo.Abp.Options; + +/// +/// This Options manager is similar to Microsoft UnnamedOptionsManager but without the locking mechanism. +/// Prevent deadlocks when accessing options in multiple threads. +/// +/// +public class AbpUnnamedOptionsManager : IOptions + where TOptions : class +{ + private readonly IOptionsFactory _factory; + private volatile TOptions? _value; + + public AbpUnnamedOptionsManager(IOptionsFactory factory) + { + _factory = factory; + } + + public TOptions Value + { + get + { + if (_value is TOptions value) + { + return value; + } + + _value = _factory.Create(Microsoft.Extensions.Options.Options.DefaultName); + return _value; + } + } +} diff --git a/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpJsonNewtonsoftModule.cs b/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpJsonNewtonsoftModule.cs index ea35831d86..4b35ee5f25 100644 --- a/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpJsonNewtonsoftModule.cs +++ b/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpJsonNewtonsoftModule.cs @@ -10,7 +10,7 @@ public class AbpJsonNewtonsoftModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddOptions() + context.Services.AddAbpOptions() .Configure((options, rootServiceProvider) => { options.JsonSerializerSettings.ContractResolver = new AbpCamelCasePropertyNamesContractResolver( diff --git a/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/AbpJsonSystemTextJsonModule.cs b/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/AbpJsonSystemTextJsonModule.cs index 0a5066cec1..2679cca96c 100644 --- a/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/AbpJsonSystemTextJsonModule.cs +++ b/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/AbpJsonSystemTextJsonModule.cs @@ -15,7 +15,7 @@ public class AbpJsonSystemTextJsonModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddOptions() + context.Services.AddAbpOptions() .Configure((options, rootServiceProvider) => { // If the user hasn't explicitly configured the encoder, use the less strict encoder that does not encode all non-ASCII characters. diff --git a/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpJsonTestModule.cs b/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpJsonTestModule.cs index 121ec62a85..5382d3db46 100644 --- a/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpJsonTestModule.cs +++ b/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpJsonTestModule.cs @@ -19,7 +19,7 @@ public class AbpJsonSystemTextJsonTestModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddOptions() + context.Services.AddAbpOptions() .Configure((options, rootServiceProvider) => { if (options.JsonSerializerOptions.TypeInfoResolver != null) @@ -43,7 +43,7 @@ public class AbpJsonNewtonsoftTestModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddOptions() + context.Services.AddAbpOptions() .Configure((options, rootServiceProvider) => { options.JsonSerializerSettings.ContractResolver = new AbpCamelCasePropertyNamesContractResolver( diff --git a/framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/MemoryDb/AbpMemoryDbTestModule.cs b/framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/MemoryDb/AbpMemoryDbTestModule.cs index 4435f1dcf0..a2234aa14a 100644 --- a/framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/MemoryDb/AbpMemoryDbTestModule.cs +++ b/framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/MemoryDb/AbpMemoryDbTestModule.cs @@ -34,7 +34,7 @@ public class AbpMemoryDbTestModule : AbpModule options.AddRepository(); }); - context.Services.AddOptions() + context.Services.AddAbpOptions() .Configure((options, rootServiceProvider) => { options.JsonSerializerOptions.Converters.Add(new EntityJsonConverter()); diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs index d261068adb..67b5d0bfa7 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs @@ -47,7 +47,7 @@ public class AbpIdentityAspNetCoreModule : AbpModule public override void PostConfigureServices(ServiceConfigurationContext context) { - context.Services.AddOptions() + context.Services.AddAbpOptions() .Configure((securityStampValidatorOptions, serviceProvider) => { var abpRefreshingPrincipalOptions = serviceProvider.GetRequiredService>().Value;