DI022

Service Instance Reused Across Handler Invocations

Two tiers. First, the same capture shape as DI021 on a sink whose concurrency is controlled by a configuration knob that cannot be proven at compile time — canonically ServiceBusProcessor where MaxConcurrentCalls comes from configuration or is left at its default of 1, and RabbitMQ consumers (EventingBasicConsumer/AsyncEventingBasicConsumer) where ConsumerDispatchConcurrency lives on the ConnectionFactory several hops from the consumer; a constant above 1 on the actual SDK property upgrades the report to DI021, while unrelated same-named user properties do not. Second, the scoped-lifetime tier: a service outside the non-thread-safe catalog whose effective registration is scoped, captured into any concurrently-invoked handler — the capture itself is the lifetime violation, so the report stays Info regardless of the sink's knob. Singleton-registered and unregistered captures stay silent.

Info Default severity · Code fix: Yes

Why it matters

If the knob is ever raised above 1 this becomes the DI021 concurrency crash. Even with sequential dispatch, one instance accumulates state across all messages: an EF Core change tracker grows without bound, and a failed SaveChanges poisons every subsequent message. DI022 reports at Info severity because the concurrency claim is conditional; raise it per team policy with dotnet_diagnostic.DI022.severity = warning. When MaxConcurrentCalls is a compile-time constant above 1 the diagnostic upgrades to DI021; when it is provably 1, both rules stay silent. Knob proofs follow same-file non-virtual helper methods that return a fresh options creation (var options = CreateOptions();), so concurrency configured in a sibling factory method is proven too; virtual helpers, parameter-driven values, and shared-instance returns stay unproven.

Code fix

Yes. Same scope-per-invocation rewrite as DI021.

Repo sample extraction

Examples pulled from the sample app

Open full sample file

Sample app config-gated capture info

    public class Bad_ProcessorCaptureWithUnprovenConcurrency
    {
        private readonly DbConnection _connection;

        public Bad_ProcessorCaptureWithUnprovenConcurrency(DbConnection connection)
        {
            _connection = connection;
        }

        public void Start(ServiceBusProcessor processor)
        {
            processor.ProcessMessageAsync += args =>
            {
                // [DI022] '_connection' is captured once and reused across all invocations
                var command = _connection.CreateCommand();
                command.CommandText = "SELECT 1";
                command.ExecuteNonQuery();
                return Task.CompletedTask;
            };
        }
    }

Sample app per-message pattern

    public class Good_ConnectionPerMessage
    {
        private readonly Func<DbConnection> _connectionFactory;

        public Good_ConnectionPerMessage(Func<DbConnection> connectionFactory)
        {
            _connectionFactory = connectionFactory;
        }

        public void Start(ServiceBusProcessor processor)
        {
            processor.ProcessMessageAsync += args =>
            {
                // Create the non-thread-safe resource per message instead of sharing one instance.
                using var connection = _connectionFactory();
                using var command = connection.CreateCommand();
                command.CommandText = "SELECT 1";
                command.ExecuteNonQuery();
                return Task.CompletedTask;
            };
        }
    }

Related guides

  • No problem-guide pages point here yet.

Nearby diagnostics

Other rules in this family

All 37 rules