Samra
Samra

Reputation: 2013

cache TryGetValue versus Get

This is how I am implementing my CacheManager. The problem I am facing is that TryGetValue will always return null in RemoveFromCache function. This function is called after one of the tokens has expired and so I am trying to clear that token from the List in cache, while GetAllTokens is returning full list of all tokens. AddTokenToCache is working correctly.

Its a WebAPI on ASPNET-Core 3.0

CacheManager.cs

public class CacheManager : ICacheManager
{
    private IMemoryCache _cache;

    public CacheManager(IMemoryCache cache) {
        _cache = cache;

    }        
    public void AddTokenToCache(string appName, string tokenString)
    {
        List<Token> tokens = new List<Token>();

        //save this token against the application record in-memory
        if (!_cache.TryGetValue(CacheHelper.CacheKey_Tokens, out tokens))
        {
            if (tokens == null)
                tokens = new List<Token>();

        }

        tokens.Add(new Token
        {
            AppName = appName,
            GeneratedAt = DateTime.Now,
            TokenId = tokenString
        });

        // Set cache options.
        var cacheEntryOptions = new MemoryCacheEntryOptions()
        ;//    .SetSlidingExpiration(TimeSpan.FromSeconds(180)); //3 minutes

        _cache.Set(CacheHelper.CacheKey_Tokens, tokens, cacheEntryOptions);
    }

    public List<Token> GetAllTokens()
    {
        return _cache.Get<List<Token>>(CacheHelper.CacheKey_Tokens);
    }

    public bool RemoveFromCache(string tokenId)
    {

        List<Token> tokens = new List<Token>();            

        //remove this token from memory            
        if (!_cache.TryGetValue(CacheHelper.CacheKey_Tokens, out tokens)) {
            return false;
        }
        else
        {
            if (tokens != null && tokens.Count > 0)
            {
                //_logger.LogInfo("Processing token");
                //trimming quotations from the string
                tokenId = tokenId.Substring(1, tokenId.Length - 2);
                int index = tokens.FindIndex(t => t.TokenId == tokenId);

                if (index >= 0)
                    tokens.RemoveAt(index);

                var cacheEntryOptions = new MemoryCacheEntryOptions();
                _cache.Set(CacheHelper.CacheKey_Tokens, tokens, cacheEntryOptions);

                return true;
            }                
        }

        return false;
    }
}

My calling sequence is:

Startup.cs

public void ConfigureServices(IServiceCollection services)
    { 
        services.AddSingleton<ILoggerManager, LoggerManager>();

        services.AddMemoryCache();
        services.AddDbContext<GEContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
        services.AddControllers();
        services.AddRazorPages();
        services.AddSingleton<ICacheManager, CacheManager>();

        RegisterHandlerforTokenExpiredEvent(services);
        //other code removed for brevity
    }
public void RegisterHandlerforTokenExpiredEvent(IServiceCollection services)
    {
        var sp = services.BuildServiceProvider();

        var jwtManager = sp.GetService<IJWTAuthenticationManager>(); //publisher            
        var cacheManager = sp.GetService<ICacheManager>(); //subscriber

        jwtManager.TokenExpired += cacheManager.OnTokenExpired;            
    }

Upvotes: 0

Views: 1324

Answers (1)

itminus
itminus

Reputation: 25380

That's because you built another ServiceProvider by services.BuildServiceProvider():

public void RegisterHandlerforTokenExpiredEvent(IServiceCollection services)

{
    var sp = services.BuildServiceProvider();  // this is a different service provider from the default one built by ASP.NET Core itself.

    var jwtManager = sp.GetService<IJWTAuthenticationManager>(); //publisher            
    var cacheManager = sp.GetService<ICacheManager>(); //subscriber
    // it doesn't work because the cacheManager is not the same instance that you use in the controllers
    jwtManager.TokenExpired += cacheManager.OnTokenExpired;            
}

As a result, the ICacheManager instance you get is NOT the same singleton that you inject in Controllers/Other Services. In other words, you'll have two different ICacheManager instance !

As a golden rule, DO NOT build another copy of ServiceProvider by services.BuildServiceProvider() in you application layer code unless you're pretty sure it's fine for you.

How to fix

  1. Instead of building another copy of service provider and then getting another instance, you should always use IoC instead of Service Locator Pattern.
  2. Seems that your JWTAuthenticationManager is a singleton and you want to bind the Event handler at startup-time. If that's the case, you could register an HostedService.

    public class MyHostedService : IHostedService
    {
        private readonly IJWTAuthenticationManager _jWTAuthManager;
        private readonly ICacheManager _cacheManager;
    
        // suppose your IJWTAuthenticationManager  is a singleton service
        public MyHostedService(IJWTAuthenticationManager jWTAuthManager, ICacheManager cacheManager)
        {
            this._jWTAuthManager = jWTAuthManager;
            this._cacheManager = cacheManager;
        }
    
        public Task StartAsync(CancellationToken cancellationToken)
        {
            this._jWTAuthManager.TokenExpired += this._cacheManager.OnTokenExpired;
            return Task.CompletedTask;
        }
    
        public Task StopAsync(CancellationToken cancellationToken)
        {
            this._jWTAuthManager.TokenExpired -= this._cacheManager.OnTokenExpired;
            return Task.CompletedTask;
        }
    }
    

    and register this service within Startup:

    services.AddHostedService<MyHostedService>();
    

Another way that doesn't need HostedService and starts at start-up time:

Get the service and bind the event before Host.Run():

public static void Main(string[] args)
{
    var host = CreateHostBuilder(args).Build();
    var jwtMgr = host.Services.GetRequiredService<IJWTAuthenticationManager>();
    var cacheMgr = host.Services.GetRequiredService<ICacheManager>();
    jwtMgr.TokenExpired = cacheMgr.OnTokenExpired;
    host.Run();
}

Upvotes: 1

Related Questions