Skip to content

Commit 7162c45

Browse files
authored
Fix memory leak with IOptionsMonitor.OnChange and non-singleton registered components (closes #20709 for 16/17) (#20723)
* Fix memory leak with IOptionsMonitor.OnChange and non-singleton registered components. * Dispose disposable data editors in ValueEditorCache. * Removed unnecessary refactoring and clarified code comments.
1 parent f4a7a2d commit 7162c45

File tree

11 files changed

+61
-50
lines changed

11 files changed

+61
-50
lines changed

src/Umbraco.Cms.Api.Management/Controllers/Help/GetHelpController.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class GetHelpController : HelpControllerBase
1717
{
1818
private readonly ILogger<GetHelpController> _logger;
1919
private readonly IJsonSerializer _jsonSerializer;
20-
private HelpPageSettings _helpPageSettings;
20+
private readonly HelpPageSettings _helpPageSettings;
2121

2222
public GetHelpController(
2323
IOptionsMonitor<HelpPageSettings> helpPageSettings,
@@ -27,11 +27,8 @@ public GetHelpController(
2727
_logger = logger;
2828
_jsonSerializer = jsonSerializer;
2929
_helpPageSettings = helpPageSettings.CurrentValue;
30-
helpPageSettings.OnChange(UpdateHelpPageSettings);
3130
}
3231

33-
private void UpdateHelpPageSettings(HelpPageSettings settings) => _helpPageSettings = settings;
34-
3532
[HttpGet]
3633
[MapToApiVersion("1.0")]
3734
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]

src/Umbraco.Cms.Api.Management/Controllers/ModelsBuilder/BuildModelsBuilderController.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
1-
using System;
2-
using System.Threading.Tasks;
31
using Asp.Versioning;
42
using Microsoft.AspNetCore.Http;
53
using Microsoft.AspNetCore.Mvc;
6-
using Microsoft.Extensions.DependencyInjection;
74
using Microsoft.Extensions.Options;
85
using Umbraco.Cms.Core;
96
using Umbraco.Cms.Core.Configuration.Models;
107
using Umbraco.Cms.Infrastructure.ModelsBuilder;
118
using Umbraco.Cms.Infrastructure.ModelsBuilder.Building;
12-
using Umbraco.Extensions;
139

1410
namespace Umbraco.Cms.Api.Management.Controllers.ModelsBuilder;
1511

1612
[ApiVersion("1.0")]
1713
public class BuildModelsBuilderController : ModelsBuilderControllerBase
1814
{
19-
private ModelsBuilderSettings _modelsBuilderSettings;
15+
private readonly ModelsBuilderSettings _modelsBuilderSettings;
2016
private readonly ModelsGenerationError _mbErrors;
2117
private readonly IModelsGenerator _modelGenerator;
2218

@@ -28,8 +24,6 @@ public BuildModelsBuilderController(
2824
_mbErrors = mbErrors;
2925
_modelGenerator = modelGenerator;
3026
_modelsBuilderSettings = modelsBuilderSettings.CurrentValue;
31-
32-
modelsBuilderSettings.OnChange(x => _modelsBuilderSettings = x);
3327
}
3428

3529
[HttpPost("build")]

src/Umbraco.Cms.Api.Management/Factories/ModelsBuilderPresentationFactory.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,24 @@
1-
using System.Text;
21
using Microsoft.Extensions.Options;
2+
using Umbraco.Cms.Api.Management.ViewModels.ModelsBuilderDashboard;
33
using Umbraco.Cms.Core;
4-
using Umbraco.Cms.Core.Configuration;
54
using Umbraco.Cms.Core.Configuration.Models;
65
using Umbraco.Cms.Infrastructure.ModelsBuilder;
7-
using Umbraco.Cms.Api.Management.ViewModels.ModelsBuilderDashboard;
8-
using Umbraco.Extensions;
96

107
namespace Umbraco.Cms.Api.Management.Factories;
118

129
public class ModelsBuilderPresentationFactory : IModelsBuilderPresentationFactory
1310
{
14-
private ModelsBuilderSettings _modelsBuilderSettings;
1511
private readonly ModelsGenerationError _mbErrors;
1612
private readonly OutOfDateModelsStatus _outOfDateModels;
13+
private readonly ModelsBuilderSettings _modelsBuilderSettings;
1714

1815
public ModelsBuilderPresentationFactory(IOptionsMonitor<ModelsBuilderSettings> modelsBuilderSettings, ModelsGenerationError mbErrors, OutOfDateModelsStatus outOfDateModels)
1916
{
2017
_mbErrors = mbErrors;
2118
_outOfDateModels = outOfDateModels;
2219
_modelsBuilderSettings = modelsBuilderSettings.CurrentValue;
23-
24-
modelsBuilderSettings.OnChange(x => _modelsBuilderSettings = x);
2520
}
2621

27-
2822
public ModelsBuilderResponseModel Create() =>
2923
new()
3024
{

src/Umbraco.Core/Cache/ValueEditorCache.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,15 @@ public void ClearCache(IEnumerable<int> dataTypeIds)
5151
{
5252
foreach (Dictionary<int, IDataValueEditor> editors in _valueEditorCache.Values)
5353
{
54-
editors.Remove(id);
54+
if (editors.TryGetValue(id, out IDataValueEditor? editor))
55+
{
56+
if (editor is IDisposable disposable)
57+
{
58+
disposable.Dispose();
59+
}
60+
61+
editors.Remove(id);
62+
}
5563
}
5664
}
5765
}

src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
using Microsoft.Extensions.DependencyInjection;
21
using Microsoft.Extensions.Options;
32
using Umbraco.Cms.Core.Configuration.Models;
4-
using Umbraco.Cms.Core.DependencyInjection;
53
using Umbraco.Cms.Core.Models.PublishedContent;
64
using Umbraco.Cms.Core.PublishedCache;
7-
using Umbraco.Cms.Core.Services;
85
using Umbraco.Extensions;
96

107
namespace Umbraco.Cms.Core.DeliveryApi;

src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See LICENSE for more details.
33

44
using Microsoft.Extensions.Options;
5+
using Umbraco.Cms.Core.Cache;
56
using Umbraco.Cms.Core.Configuration.Models;
67
using Umbraco.Cms.Core.IO;
78
using Umbraco.Cms.Core.Models;
@@ -22,14 +23,22 @@ namespace Umbraco.Cms.Core.PropertyEditors;
2223
/// <summary>
2324
/// The value editor for the file upload property editor.
2425
/// </summary>
25-
internal sealed class FileUploadPropertyValueEditor : DataValueEditor
26+
/// <remarks>
27+
/// As this class is loaded into <see cref="ValueEditorCache"/> which can be cleared, it needs
28+
/// to be disposable in order to properly clean up resources such as
29+
/// the settings change subscription and avoid a memory leak.
30+
/// </remarks>
31+
internal sealed class FileUploadPropertyValueEditor : DataValueEditor, IDisposable
2632
{
2733
private readonly MediaFileManager _mediaFileManager;
2834
private readonly ITemporaryFileService _temporaryFileService;
2935
private readonly IScopeProvider _scopeProvider;
3036
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
3137
private readonly FileUploadValueParser _valueParser;
38+
3239
private ContentSettings _contentSettings;
40+
private readonly IDisposable? _contentSettingsChangeSubscription;
41+
3342

3443
/// <summary>
3544
/// Initializes a new instance of the <see cref="FileUploadPropertyValueEditor"/> class.
@@ -46,14 +55,14 @@ public FileUploadPropertyValueEditor(
4655
IFileStreamSecurityValidator fileStreamSecurityValidator)
4756
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
4857
{
49-
_mediaFileManager = mediaFileManager ?? throw new ArgumentNullException(nameof(mediaFileManager));
58+
_mediaFileManager = mediaFileManager;
5059
_temporaryFileService = temporaryFileService;
5160
_scopeProvider = scopeProvider;
5261
_fileStreamSecurityValidator = fileStreamSecurityValidator;
5362
_valueParser = new FileUploadValueParser(jsonSerializer);
5463

55-
_contentSettings = contentSettings.CurrentValue ?? throw new ArgumentNullException(nameof(contentSettings));
56-
contentSettings.OnChange(x => _contentSettings = x);
64+
_contentSettings = contentSettings.CurrentValue;
65+
_contentSettingsChangeSubscription = contentSettings.OnChange(x => _contentSettings = x);
5766

5867
Validators.Add(new TemporaryFileUploadValidator(
5968
() => _contentSettings,
@@ -216,4 +225,6 @@ private string GetMediaPath(TemporaryFileModel file, object? dataTypeConfigurati
216225
// in case we are using the old path scheme, try to re-use numbers (bah...)
217226
return _mediaFileManager.GetMediaPath(file.FileName, contentKey, propertyTypeKey);
218227
}
228+
229+
public void Dispose() => _contentSettingsChangeSubscription?.Dispose();
219230
}

src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
// Copyright (c) Umbraco.
22
// See LICENSE for more details.
33

4-
using Microsoft.Extensions.DependencyInjection;
54
using Microsoft.Extensions.Logging;
65
using Microsoft.Extensions.Options;
76
using Umbraco.Cms.Core.Configuration.Models;
8-
using Umbraco.Cms.Core.DependencyInjection;
97
using Umbraco.Cms.Core.Events;
108
using Umbraco.Cms.Core.IO;
119
using Umbraco.Cms.Core.Media;
@@ -26,7 +24,8 @@ namespace Umbraco.Cms.Core.PropertyEditors;
2624
Constants.PropertyEditors.Aliases.ImageCropper,
2725
ValueType = ValueTypes.Json,
2826
ValueEditorIsReusable = true)]
29-
public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator,
27+
public class ImageCropperPropertyEditor : DataEditor,
28+
IMediaUrlGenerator,
3029
INotificationHandler<ContentCopiedNotification>,
3130
INotificationHandler<ContentDeletedNotification>,
3231
INotificationHandler<MediaDeletedNotification>,
@@ -40,9 +39,10 @@ public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator,
4039
private readonly IIOHelper _ioHelper;
4140
private readonly ILogger<ImageCropperPropertyEditor> _logger;
4241
private readonly MediaFileManager _mediaFileManager;
43-
private ContentSettings _contentSettings;
4442
private readonly IJsonSerializer _jsonSerializer;
4543

44+
private ContentSettings _contentSettings;
45+
4646
/// <summary>
4747
/// Initializes a new instance of the <see cref="ImageCropperPropertyEditor" /> class.
4848
/// </summary>
@@ -57,15 +57,14 @@ public ImageCropperPropertyEditor(
5757
IJsonSerializer jsonSerializer)
5858
: base(dataValueEditorFactory)
5959
{
60-
_mediaFileManager = mediaFileManager ?? throw new ArgumentNullException(nameof(mediaFileManager));
61-
_contentSettings = contentSettings.CurrentValue ?? throw new ArgumentNullException(nameof(contentSettings));
62-
_ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper));
63-
_autoFillProperties =
64-
uploadAutoFillProperties ?? throw new ArgumentNullException(nameof(uploadAutoFillProperties));
60+
_mediaFileManager = mediaFileManager;
61+
_ioHelper = ioHelper;
62+
_autoFillProperties = uploadAutoFillProperties;
6563
_contentService = contentService;
6664
_jsonSerializer = jsonSerializer;
6765
_logger = loggerFactory.CreateLogger<ImageCropperPropertyEditor>();
6866

67+
_contentSettings = contentSettings.CurrentValue;
6968
contentSettings.OnChange(x => _contentSettings = x);
7069

7170
SupportsReadOnly = true;

src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,24 @@ namespace Umbraco.Cms.Core.PropertyEditors;
2222
/// <summary>
2323
/// The value editor for the image cropper property editor.
2424
/// </summary>
25-
internal sealed class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core vs web?
25+
/// <remarks>
26+
/// As this class is loaded into <see cref="ValueEditorCache"/> which can be cleared, it needs
27+
/// to be disposable in order to properly clean up resources such as
28+
/// the settings change subscription and avoid a memory leak.
29+
/// </remarks>
30+
internal sealed class ImageCropperPropertyValueEditor : DataValueEditor, IDisposable
2631
{
2732
private readonly IDataTypeConfigurationCache _dataTypeConfigurationCache;
2833
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
2934
private readonly ILogger<ImageCropperPropertyValueEditor> _logger;
3035
private readonly MediaFileManager _mediaFileManager;
3136
private readonly IJsonSerializer _jsonSerializer;
32-
private ContentSettings _contentSettings;
3337
private readonly ITemporaryFileService _temporaryFileService;
3438
private readonly IScopeProvider _scopeProvider;
3539

40+
private ContentSettings _contentSettings;
41+
private readonly IDisposable? _contentSettingsChangeSubscription;
42+
3643
public ImageCropperPropertyValueEditor(
3744
DataEditorAttribute attribute,
3845
ILogger<ImageCropperPropertyValueEditor> logger,
@@ -50,12 +57,13 @@ public ImageCropperPropertyValueEditor(
5057
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
5158
_mediaFileManager = mediaFileSystem ?? throw new ArgumentNullException(nameof(mediaFileSystem));
5259
_jsonSerializer = jsonSerializer;
53-
_contentSettings = contentSettings.CurrentValue;
5460
_temporaryFileService = temporaryFileService;
5561
_scopeProvider = scopeProvider;
5662
_fileStreamSecurityValidator = fileStreamSecurityValidator;
5763
_dataTypeConfigurationCache = dataTypeConfigurationCache;
58-
contentSettings.OnChange(x => _contentSettings = x);
64+
65+
_contentSettings = contentSettings.CurrentValue;
66+
_contentSettingsChangeSubscription = contentSettings.OnChange(x => _contentSettings = x);
5967

6068
Validators.Add(new TemporaryFileUploadValidator(() => _contentSettings, TryParseTemporaryFileKey, TryGetTemporaryFile));
6169
}
@@ -267,4 +275,6 @@ public override string ConvertDbToString(IPropertyType propertyType, object? val
267275

268276
return filepath;
269277
}
278+
279+
public void Dispose() => _contentSettingsChangeSubscription?.Dispose();
270280
}

src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
2626
/// used dynamically.
2727
/// </summary>
2828
[DefaultPropertyValueConverter]
29-
public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDeliveryApiPropertyValueConverter
29+
public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDeliveryApiPropertyValueConverter, IDisposable
3030
{
3131
private readonly HtmlImageSourceParser _imageSourceParser;
3232
private readonly HtmlLocalLinkParser _linkParser;
@@ -41,7 +41,9 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
4141
private readonly RichTextBlockPropertyValueConstructorCache _constructorCache;
4242
private readonly IVariationContextAccessor _variationContextAccessor;
4343
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
44+
4445
private DeliveryApiSettings _deliveryApiSettings;
46+
private readonly IDisposable? _deliveryApiSettingsChangeSubscription;
4547

4648
public RteBlockRenderingValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser,
4749
IApiRichTextElementParser apiRichTextElementParser, IApiRichTextMarkupParser apiRichTextMarkupParser,
@@ -62,8 +64,9 @@ public RteBlockRenderingValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlPa
6264
_logger = logger;
6365
_variationContextAccessor = variationContextAccessor;
6466
_blockEditorVarianceHandler = blockEditorVarianceHandler;
67+
6568
_deliveryApiSettings = deliveryApiSettingsMonitor.CurrentValue;
66-
deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings);
69+
_deliveryApiSettingsChangeSubscription = deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings);
6770
}
6871

6972
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) =>
@@ -250,4 +253,6 @@ private sealed class RichTextEditorIntermediateValue : IRichTextEditorIntermedia
250253

251254
public required RichTextBlockModel? RichTextBlockModel { get; set; }
252255
}
256+
257+
public void Dispose() => _deliveryApiSettingsChangeSubscription?.Dispose();
253258
}

src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.DeliveryApi.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Microsoft.Extensions.Logging;
1+
using Microsoft.Extensions.Logging;
22
using Microsoft.Extensions.Options;
33
using Umbraco.Cms.Core.Cache;
44
using Umbraco.Cms.Core.Configuration.Models;
@@ -17,7 +17,7 @@ internal sealed class DeliveryApiContentIndexingNotificationHandler :
1717
{
1818
private readonly IDeliveryApiIndexingHandler _deliveryApiIndexingHandler;
1919
private readonly ILogger<DeliveryApiContentIndexingNotificationHandler> _logger;
20-
private DeliveryApiSettings _deliveryApiSettings;
20+
private readonly DeliveryApiSettings _deliveryApiSettings;
2121

2222
public DeliveryApiContentIndexingNotificationHandler(
2323
IDeliveryApiIndexingHandler deliveryApiIndexingHandler,
@@ -27,7 +27,6 @@ public DeliveryApiContentIndexingNotificationHandler(
2727
_deliveryApiIndexingHandler = deliveryApiIndexingHandler;
2828
_logger = logger;
2929
_deliveryApiSettings = deliveryApiSettings.CurrentValue;
30-
deliveryApiSettings.OnChange(settings => _deliveryApiSettings = settings);
3130
}
3231

3332
public void Handle(ContentCacheRefresherNotification notification)

0 commit comments

Comments
 (0)