Mad Eddie
Mad Eddie

Reputation: 1217

netcore 2.1 Distributed cache with Scoped repository

Need some help.

I have a .netcore 2.1 API which is secured via Azure Bearer token from its clients. I am wanting to collect user information from the bearer token of clients and store it in a SQL database so that I can tag entries within the database if they are being added/deleted/edited etc. For the SQL table joins I therefore need the user imformation in SQL.

Below is my implementation of a Cache Service using IDistributedCache. On Init I am trying to load all currently stored users from the SQL DB in to the cache, then added to it when new users connect in.

To capture the connections across the entire API I was using a TypeFilterAttribute to execute OnActionExecuting.

The problem is that the CacheService is a singleton and is calling the UserRepository - which is scoped. This isn't allowed.

Any thoughts?

startup.cs

public void ConfigureServices(IServiceCollection services)
        {
...
            // Context
            services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.TryAddSingleton<CacheService>();

            // Repositories
            services.TryAddScoped<IUserRepository, UserRepository>();                    

            services.AddDistributedMemoryCache();

            services.AddMvc(
               opts => opts.Filters.Add(new HttpInterceptor())
                )
...

CacheService.cs

public class CacheService
    {
        private readonly IDistributedCache _cache;
        private readonly IUserRepository _userRepository;

        public CacheService(
            IDistributedCache cache,
          [FromServices]  IUserRepository userRepository
            )
        {
            _cache = cache;
            _userRepository = userRepository;

            // Populate cache from DB
            var users = _userRepository.GetAll().Result;
            foreach (var u in users)
            {
                if (_cache.GetAsync(u.Username).Result == null)
                {
                    var profileSerialised = JsonConvert.SerializeObject(UserToUserProfile(u));
                    var entry = Encoding.UTF8.GetBytes(profileSerialised);
                    _cache.SetAsync(u.Username, entry, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) });
                }
            }
        }

HttpInterceptor.cs

 public class HttpInterceptor : TypeFilterAttribute
    {
        public HttpInterceptor() : base(typeof(IHttpInterceptor))
        {

        }

        private class IHttpInterceptor : IActionFilter
        {
            private readonly CacheService _cache;
            private readonly IUserRepository _userRepository;

            public IHttpInterceptor(
                CacheService cache,
                IUserRepository userRepository)
            {
                _cache = cache;
                _userRepository = userRepository;
            }


            public void OnActionExecuting(ActionExecutingContext context)
            {
                if (context.HttpContext.User.Identity.IsAuthenticated)
                {
                  this._cache.GetUserProfile(context.HttpContext.User.Identity.Name);
                }
            }

Upvotes: 0

Views: 1075

Answers (1)

Chris Pratt
Chris Pratt

Reputation: 239380

First, you're looking at this upside-down and backwards. Having some service add stuff to the cache and then having other code just assume that stuff is in the cache ready to go is a recipe for disaster. Instead, have your dependent code literally request the data it needs, and then, if you want to cache it, do it the method that fetches the data. That way your app code remains agnostic about where the data is coming from; it just calls a method and it gets the data it wants. Under the hood, it's either pulled from the database or the cache, depending on which is available/preferred.

Your cache service has serious issues anyways. First, it should not be a singleton in the first place. There's no reason for it to be, and since you're dealing with scoped services inside, you're only making things more difficult than they need to be. Second, you should never ever utilize I/O in a constructor. Only simple variable/prop initialization should be done there. Anything that requires actual work should go into a method. If you truly want to do something on initialization, then you should implement a factory pattern. For example, you might have something like a CacheServiceFactory with a Create method that returns a fully instantiated CacheService including calling any methods that do actual work.

With the disclaimers aside, in general, to use a scoped service in a singleton, you must create a scope. This must be done every time you want to utilize the service; you cannot persist the service to an ivar on your singleton class. Simply, you inject IServiceProvider into your class, which is itself singleton-scoped, so you'll have no problems with that. Then, when you need to utilize a scoped service:

using (var scope = provider.CreateScope())
{
    var repo = scope.ServiceProvider.GetRequiredService<IUserRepository>();
    // do something with repo
}

This is called the service locator anti-pattern. It's called such because it's something you should really avoid doing. Sometimes that's no always possible. However, more often than not, you can simply design things in a different way: such as making the service scoped itself.

Upvotes: 1

Related Questions