Part 1 : Sharing State Across Dynamically Loaded Components in .NET - Singleton Cache Manager

Part 1 : Sharing State Across Dynamically Loaded Components in .NET - Singleton Cache Manager

One effective solution for sharing state across components is to use a singleton cache manager. This manager ensures that all dynamically loaded components access the same shared cache instance, with proper locking mechanisms to ensure thread safety.

Some Background

Our aim is to create a single instance of our cache and reuse it across different components. To achieve this, we will be using the Singleton Design Pattern, which is part of the Creational Design Patterns. These patterns offer various ways to create objects to increase flexibility and reusability in our codebase.


Step 1: Implementing a Thread-Safe Singleton Cache Manager

using System;
using System.Collections.Generic;

public class CacheManager
{
    private static readonly Lazy<CacheManager> _instance = new Lazy<CacheManager>(()=>new CacheManager());
    private readonly Dictionary<string, object> _cache;

    private CacheManager()
    {
        _cache = new Dictionary<string, object>();
    }

    public static CacheManager Instance
    {
        get
        {
            return _instance.Value;
        }
    }

    public void AddToCache(string key, object value)
    {
            _cache[key] = value;
            Console.WriteLine($"Added to cache: {key} = {value}");
    }

    public object GetFromCache(string key)
    {
            _cache.TryGetValue(key, out var value);
            return value;
    }
}        


Explanation:

  • Thread-Safe Access: By using Lazy initialization, we ensure that the cache can only be accessed by one thread at a time, preventing race conditions.
  • Singleton Pattern: The CacheManager ensures that there is only one instance of the cache manager, accessible across the entire application.
  • Shared State: Components can use this cache to store and retrieve shared data, such as configuration settings or cached results.

Step 2: Defining a Component Interface

Each dynamically loaded component will need a standard way to interact with the system. To achieve this, we define a simple interface that all components will implement:

public interface IComponent
{
    void Execute();
}
        

This IComponent interface ensures that each component will have an Execute method, which is invoked when the component is loaded.

Step 3: Writing the Components

Let’s implement two components that share a state using the CacheManager. These components will be loaded dynamically at runtime, and they will both interact with the shared cache.

Component A:

using System;

public class ComponentA : IComponent
{
    public void Execute()
    {
        Console.WriteLine("ComponentA is executing.");
        CacheManager.Instance.AddToCache("ComponentAKey", "ComponentAValue");
        var cachedValue = CacheManager.Instance.GetFromCache("ComponentBKey");
        Console.WriteLine($"ComponentA retrieved value from ComponentB: {cachedValue}");
    }
}
        

Component B:

using System;

public class ComponentB : IComponent
{
    public void Execute()
    {
        Console.WriteLine("ComponentB is executing.");
        CacheManager.Instance.AddToCache("ComponentBKey", "ComponentBValue");
        var cachedValue = CacheManager.Instance.GetFromCache("ComponentAKey");
        Console.WriteLine($"ComponentB retrieved value from ComponentA: {cachedValue}");
    }
}        

Step 4: Loading Components and Executing

Now, let’s load the components and invoke their Execute methods.

        // Instantiate the components directly
        IComponent componentA = new ComponentA();
        IComponent componentB = new ComponentB();

        // Execute each component
        componentA.Execute();
        componentB.Execute();
        

Expected Output:

ComponentA is executing.
Added to cache: ComponentAKey = ComponentAValue
ComponentA retrieved value from ComponentB: ComponentBValue
ComponentB is executing.
Added to cache: ComponentBKey = ComponentBValue
ComponentB retrieved value from ComponentA: ComponentAValue
Execution completed.        

This shows that both components can share state through the CacheManager, even though they were loaded dynamically at runtime.

Conclusion

With Singleton Cache Manager, we can ensure that all components have access to shared data in a thread-safe manner. This approach allows us to dynamically load components at runtime while maintaining consistency and preventing race conditions.

The solution provided here is scalable and can be adaptable for various real-world scenarios, from dynamically loaded modules to service-oriented architectures.

To view or add a comment, sign in

More articles by Kolawole Abobade

Explore content categories