Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Memory leak when calling WithSubstitution multiple times #28

Copy link
Copy link

Description

@Anton-Pronkin
Issue body actions

Description

When WithSubstitution is called multiple times on the same IConfigurationBuilder, it causes recursive accumulation of configuration sources, leading to excessive memory usage.

Root Cause

In SubstitutingConfigurationSource.Build(), when building valueRoot, the code iterates through ALL sources in builder.Sources except itself and creates a new ConfigurationBuilder with copies of those sources. Each subsequent call to WithSubstitution adds a new SubstitutingConfigurationSource that, when built, will copy all previously added sources again.

Demo

To see allocated memory depending on the number of added WithSubstitution calls, you can try:

const int iterations = 5;

// Emulate configurations
var configurations = Enumerable.Range(0, 500)
    .Select(key => new KeyValuePair<string, string>(key.ToString(), null));

// Emulate multiple sources
var configuration = new ConfigurationBuilder();
for (var i = 0; i < iterations; i++)
{
    configuration.WithSubstitution(c => c.AddInMemoryCollection(configurations));
}

configuration.Build();

// Check memory usage
var currentProcess = Process.GetCurrentProcess();
var usageInMb = currentProcess.WorkingSet64  / 1024 / 1024;

On my machine, the results were:

Sources Memory usage in MB
2 48
3 49
4 52
5 61
6 139
7 661
8 4947
9 > 32 GB, OOM

In real projects, there could be many sources, and this behavior may cause memory problems.

Suggestions

  1. First of all, I would recommend adding a notification in the README to use a single WithSubstitution if possible.

Important

Avoid the following pattern to prevent memory leaks:

builder
    .WithSubstitution(c => c.AddJsonFile("appsettings1.json"))
    .WithSubstitution(c => c.AddJsonFile("appsettings2.json"))
    .WithSubstitution(c => c.AddJsonFile("appsettings3.json"));

Use a single WithSubstitution with multiple sources instead:

builder.WithSubstitution(c => 
{
    c.AddJsonFile("appsettings1.json");
    c.AddJsonFile("appsettings2.json");
    c.AddJsonFile("appsettings3.json");
});

Also, I would suggest to add configurable option to prevent multiple registrations:

public class SubstitutingSettings
{
    /// <summary>
    /// Use single substituting source to prevent multiple registrations, which can cause memory leaks.
    /// Default value: false
    /// </summary>
    public bool SingleSubstitutingSource { get; set; } = false;
}
internal class SubstitutingConfigurationSource : IConfigurationSource
{
    private readonly SubstitutingSettings _settings;

    public SubstitutingConfigurationSource(..., SubstitutingSettings settings)
    {
        _settings = settings;
    }

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        // ...

        var valueBuilder = new ConfigurationBuilder();
        for (var i = 0; i < builder.Sources.Count; i++)
        {
            var source = builder.Sources[i];
            if (ReferenceEquals(source, this))
            {
                continue;
            }

            // Check if SingleSubstitutingSource is enabled
            if (_settings.SingleSubstitutingSource && source is SubstitutingConfigurationSource)
            {
                throw new InvalidOperationException("Multiple substitution sources were detected when SingleSubstitutingSource setting is enabled.");
            }

            valueBuilder.Add(source);
        }

        // ...
    }
}

Usage:

var configuration = new ConfigurationBuilder()
    .WithSubstitution(
        c => ...,
        new SubstitutingSettings { SingleSubstitutingSource = true }
    )
    .WithSubstitution(
        c => ...,
        new SubstitutingSettings { SingleSubstitutingSource = true }
    )
    .Build();

SingleSubstitutingSource could be set to true by default to prevent such memory leaks, but this might break existing behavior. A safer approach is to keep the default as false and recommend enabling it in new projects, or introduce this change in a major version update.

Discussion

What do you think? Are there more elegant solutions – for example, adding a CycleDetector at build time to prevent recursive accumulation?

I'm open to suggestions. Would a pull request be welcome?

Reactions are currently unavailable

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    Morty Proxy This is a proxified and sanitized view of the page, visit original site.