Skip to content

Commit f2589b5

Browse files
committed
Fix memory leak with IOptionsMonitor.OnChange and non-singleton registered components (closes #20709 for 13) (#20722)
* Fix memory leak with IOptionsMonitor.OnChange and non-singleton registered components. * Added XML docs. * Apply fix to DeliveryApiContentIndexingNotificationHandler. * Dispose disposable data editors in ValueEditorCache. * Removed unnecessary refactoring and clarified code comments.
1 parent 951f9a9 commit f2589b5

File tree

9 files changed

+67
-21
lines changed

9 files changed

+67
-21
lines changed

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.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs

Lines changed: 12 additions & 2 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.Editors;
@@ -16,11 +17,18 @@ namespace Umbraco.Cms.Core.PropertyEditors;
1617
/// <summary>
1718
/// The value editor for the file upload property editor.
1819
/// </summary>
19-
internal class FileUploadPropertyValueEditor : DataValueEditor
20+
/// <remarks>
21+
/// As this class is loaded into <see cref="ValueEditorCache"/> which can be cleared, it needs
22+
/// to be disposable in order to properly clean up resources such as
23+
/// the settings change subscription and avoid a memory leak.
24+
/// </remarks>
25+
internal class FileUploadPropertyValueEditor : DataValueEditor, IDisposable
2026
{
2127
private readonly MediaFileManager _mediaFileManager;
2228
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
29+
2330
private ContentSettings _contentSettings;
31+
private readonly IDisposable? _contentSettingsChangeSubscription;
2432

2533
public FileUploadPropertyValueEditor(
2634
DataEditorAttribute attribute,
@@ -36,7 +44,7 @@ public FileUploadPropertyValueEditor(
3644
_mediaFileManager = mediaFileManager ?? throw new ArgumentNullException(nameof(mediaFileManager));
3745
_fileStreamSecurityValidator = fileStreamSecurityValidator;
3846
_contentSettings = contentSettings.CurrentValue ?? throw new ArgumentNullException(nameof(contentSettings));
39-
contentSettings.OnChange(x => _contentSettings = x);
47+
_contentSettingsChangeSubscription = contentSettings.OnChange(x => _contentSettings = x);
4048
}
4149

4250
/// <summary>
@@ -163,4 +171,6 @@ public FileUploadPropertyValueEditor(
163171

164172
return filepath;
165173
}
174+
175+
public void Dispose() => _contentSettingsChangeSubscription?.Dispose();
166176
}

src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ namespace Umbraco.Cms.Core.PropertyEditors;
2121
/// <summary>
2222
/// Represents an image cropper property editor.
2323
/// </summary>
24+
/// <remarks>
25+
/// As this class is not registered with DI as a singleton, it must be disposed to release
26+
/// the settings change subscription and avoid a memory leak.
27+
/// </remarks>
2428
[DataEditor(
2529
Constants.PropertyEditors.Aliases.ImageCropper,
2630
"Image Cropper",
@@ -42,6 +46,7 @@ public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator,
4246
private readonly IIOHelper _ioHelper;
4347
private readonly ILogger<ImageCropperPropertyEditor> _logger;
4448
private readonly MediaFileManager _mediaFileManager;
49+
4550
private ContentSettings _contentSettings;
4651

4752
// Scheduled for removal in v12

src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,20 @@ namespace Umbraco.Cms.Core.PropertyEditors;
2323
/// <summary>
2424
/// The value editor for the image cropper property editor.
2525
/// </summary>
26-
internal class ImageCropperPropertyValueEditor : DataValueEditor // TODO: core vs web?
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 class ImageCropperPropertyValueEditor : DataValueEditor, IDisposable
2732
{
2833
private readonly IDataTypeConfigurationCache _dataTypeConfigurationCache;
2934
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
3035
private readonly ILogger<ImageCropperPropertyValueEditor> _logger;
3136
private readonly MediaFileManager _mediaFileManager;
37+
3238
private ContentSettings _contentSettings;
39+
private readonly IDisposable? _contentSettingsChangeSubscription;
3340

3441
public ImageCropperPropertyValueEditor(
3542
DataEditorAttribute attribute,
@@ -49,7 +56,7 @@ public ImageCropperPropertyValueEditor(
4956
_contentSettings = contentSettings.CurrentValue;
5057
_dataTypeConfigurationCache = dataTypeConfigurationCache;
5158
_fileStreamSecurityValidator = fileStreamSecurityValidator;
52-
contentSettings.OnChange(x => _contentSettings = x);
59+
_contentSettingsChangeSubscription = contentSettings.OnChange(x => _contentSettings = x);
5360
}
5461

5562
/// <summary>
@@ -252,4 +259,6 @@ public override string ConvertDbToString(IPropertyType propertyType, object? val
252259

253260
return filepath;
254261
}
262+
263+
public void Dispose() => _contentSettingsChangeSubscription?.Dispose();
255264
}

src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,19 @@
1111

1212
namespace Umbraco.Cms.Core.PropertyEditors;
1313

14-
internal class UploadFileTypeValidator : IValueValidator
14+
/// <summary>
15+
/// The value editor for file upload property editors.
16+
/// </summary>
17+
/// <remarks>
18+
/// As this class is not registered with DI as a singleton, it must be disposed to release
19+
/// the settings change subscription and avoid a memory leak.
20+
/// </remarks>
21+
internal class UploadFileTypeValidator : IValueValidator, IDisposable
1522
{
1623
private readonly ILocalizedTextService _localizedTextService;
24+
1725
private ContentSettings _contentSettings;
26+
private readonly IDisposable? _contentSettingsChangeSubscription;
1827

1928
public UploadFileTypeValidator(
2029
ILocalizedTextService localizedTextService,
@@ -23,7 +32,7 @@ public UploadFileTypeValidator(
2332
_localizedTextService = localizedTextService;
2433
_contentSettings = contentSettings.CurrentValue;
2534

26-
contentSettings.OnChange(x => _contentSettings = x);
35+
_contentSettingsChangeSubscription = contentSettings.OnChange(x => _contentSettings = x);
2736
}
2837

2938
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration)
@@ -103,4 +112,6 @@ internal static bool TryGetFileExtension(string? fileName, [MaybeNullWhen(false)
103112
extension = fileName.GetFileExtension().TrimStart(".");
104113
return true;
105114
}
115+
116+
public void Dispose() => _contentSettingsChangeSubscription?.Dispose();
106117
}

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
3131
/// A value converter for TinyMCE that will ensure any macro content is rendered properly even when
3232
/// used dynamically.
3333
/// </summary>
34+
/// <remarks>
35+
/// As this class is not registered with DI as a singleton, it must be disposed to release
36+
/// the settings change subscription and avoid a memory leak.
37+
/// </remarks>
3438
[DefaultPropertyValueConverter]
35-
public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDeliveryApiPropertyValueConverter
39+
public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDeliveryApiPropertyValueConverter, IDisposable
3640
{
3741
private readonly HtmlImageSourceParser _imageSourceParser;
3842
private readonly HtmlLocalLinkParser _linkParser;
@@ -47,7 +51,9 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel
4751
private readonly ILogger<RteMacroRenderingValueConverter> _logger;
4852
private readonly IApiElementBuilder _apiElementBuilder;
4953
private readonly RichTextBlockPropertyValueConstructorCache _constructorCache;
54+
5055
private DeliveryApiSettings _deliveryApiSettings;
56+
private readonly IDisposable? _deliveryApiSettingsChangeSubscription;
5157

5258
[Obsolete("Please use the constructor that takes all arguments. Will be removed in V14.")]
5359
public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer,
@@ -107,8 +113,9 @@ public RteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAcc
107113
_apiElementBuilder = apiElementBuilder;
108114
_constructorCache = constructorCache;
109115
_logger = logger;
116+
110117
_deliveryApiSettings = deliveryApiSettingsMonitor.CurrentValue;
111-
deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings);
118+
_deliveryApiSettingsChangeSubscription = deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings);
112119
}
113120

114121
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) =>
@@ -314,4 +321,6 @@ private class RichTextEditorIntermediateValue : IRichTextEditorIntermediateValue
314321

315322
public required RichTextBlockModel? RichTextBlockModel { get; set; }
316323
}
324+
325+
public void Dispose() => _deliveryApiSettingsChangeSubscription?.Dispose();
317326
}

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)

src/Umbraco.Web.BackOffice/Controllers/HelpController.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using Newtonsoft.Json;
77
using Umbraco.Cms.Core;
88
using Umbraco.Cms.Core.Configuration.Models;
9-
using Umbraco.Cms.Core.DependencyInjection;
109
using Umbraco.Cms.Web.Common.Attributes;
1110

1211
namespace Umbraco.Cms.Web.BackOffice.Controllers;
@@ -16,7 +15,8 @@ public class HelpController : UmbracoAuthorizedJsonController
1615
{
1716
private static HttpClient? _httpClient;
1817
private readonly ILogger<HelpController> _logger;
19-
private HelpPageSettings? _helpPageSettings;
18+
19+
private readonly HelpPageSettings? _helpPageSettings;
2020

2121
[ActivatorUtilitiesConstructor]
2222
public HelpController(
@@ -25,12 +25,9 @@ public HelpController(
2525
{
2626
_logger = logger;
2727

28-
ResetHelpPageSettings(helpPageSettings.CurrentValue);
29-
helpPageSettings.OnChange(ResetHelpPageSettings);
28+
_helpPageSettings = helpPageSettings.CurrentValue;
3029
}
3130

32-
private void ResetHelpPageSettings(HelpPageSettings settings) => _helpPageSettings = settings;
33-
3431
public async Task<List<HelpPage>> GetContextHelpForPage(string section, string tree,
3532
string baseUrl = "https://our.umbraco.com")
3633
{

src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public class ImagesController : UmbracoAuthorizedApiController
2323
{
2424
private readonly MediaFileManager _mediaFileManager;
2525
private readonly IImageUrlGenerator _imageUrlGenerator;
26-
private ContentSettings _contentSettings;
26+
private readonly ContentSettings _contentSettings;
2727

2828
[Obsolete("Use non obsolete-constructor. Scheduled for removal in Umbraco 13.")]
2929
public ImagesController(
@@ -45,8 +45,6 @@ public ImagesController(
4545
_mediaFileManager = mediaFileManager;
4646
_imageUrlGenerator = imageUrlGenerator;
4747
_contentSettings = contentSettingsMonitor.CurrentValue;
48-
49-
contentSettingsMonitor.OnChange(x => _contentSettings = x);
5048
}
5149

5250
/// <summary>

0 commit comments

Comments
 (0)