Dependency Injection and Data Race Safety

In one of our projects we use dependency injection via property wrappers as outlined here, however that falls short of providing data race safety (as mandated by SWIFT_STRICT_CONCURRENCY = complete or SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = YES):

struct InjectedValues {
    private static var current = InjectedValues() // Static property 'current' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode
    
    static subscript<K>(key: K.Type) -> K.Value where K : InjectionKey {
        get { key.currentValue }
        set { key.currentValue = newValue }
    }
    
    static subscript<T>(_ keyPath: WritableKeyPath<InjectedValues, T>) -> T {
        get { current[keyPath: keyPath] }
        set { current[keyPath: keyPath] = newValue }
    }
}

private struct NetworkProviderKey: InjectionKey {
    static var currentValue: NetworkProviding = NetworkProvider() // Static property 'current' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode
}

(code directly taken from the source)

If we do not want to make current/ currentValue immutable the way forward seems to be to isolate it to the main actor. I worry that doing so will place an undue amount of load on the main thread though.

Is there something I am missing?

IMO this is just a terrible way to do dependency injection because in the end you're relying on a "spooky-action-at-a-distance-kind-of" global dictionary / registry, which of course, being a mutable data structure shared across multiple contexts, needs to offer some locking or higher-level protection.

My first instinct would be to use locks instead of actor isolation (and make this an @unchecked Sendable class) because you never want your code to suspend when it accesses its dependencies, and you only need leaf-level locks that will never deadlock anyway.


Edit: realised that InjectedValues is stateless, and all the storage are separate statics on every key instead, in which case private static var current should just be a let, and you can do struct InjectedValues: Sendable. Additionally, the per-dependency extensions can declare nonmutating set. Because these subscripts/properties never actually mutate any field of InjectedValues (since it has none), I believe that you should be able to arrange all these annotations so that the compiler sees that there's no mutable state yet allows you to still have setters.

But this setup then means that you have to do locking per each static currentValue property.

My point is not to use dependency injection frameworks, but that's subjective. If you want to have similar behaviour to the linked article, GitHub - pointfreeco/swift-dependencies: A dependency management library inspired by SwiftUI's "environment." seems to work well with Swift Concurrency.