Search code examples
c#.net-coreservicelifetime

Scoped lifetime services become singletons when injected through controllers


I have a controller in a .NET Core application:

public FriendsController(IFriendRepository friendRepository)
        {
            this.friendRepository= friendRepository;
        }

The IFriendRepository is an interface which is implemented with the class:

public class FriendRepository : IFriendRepository {
...
}

In Startup I set it up by using the following line in ConfigureServices() :

services.AddScoped<IFriendRepository , FriendRepository >();

However, when the controller is used, FriendRepository is has the lifetime set as a singleton instead of scoped. The reason I was able to find was on this page:

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1

Under Service lifetimes, Scoped. It shows:

enter image description here

I do not understand how to use Invoke instead of a constructor. The example they use is for a custom middleware, which I at least can't wrap my head on how to interpret it for a constructor.

public class FriendRepository : IFriendRepository
    {
        private readonly ManagementDbContext dbContext;

        public FriendRepository(ManagementDbContext dbContext)
        {
            this.dbContext = dbContext;
        }

        public void Add(Friend friend)
        {
            this.dbContext.Friends.Add(friend);
        }
        public void Remove(Friend friend)
        {
            this.dbContext.Remove(friend);
        }


        public void Update(Friend friend)
        {
            this.dbContext.Update(friend);
        }
    }

The following is "GetFriends", inside FriendRepository:

public async Task<QueryResult<Friend>> GetFriendsAsync(FriendQuery queryObj)
        {
            var result = new QueryResult<Friend>();

            var query = dbContext.Friends
                        .Include(c => c.Type)
                        .AsQueryable();

            if(queryObj.TypeId.HasValue)
            {
                query = query.Where(c => c.Type.Id == queryObj.TypeId);
            }

            if(queryObj.Name != null && queryObj.Name.Length > 0)
            {
                query = query.Where(c => c.Name.Contains(queryObj.Name));
            }

            // todo add total price here
            var columnsMap = new Dictionary<string, Expression<Func<Calculation, object>>>()
            {
                ["id"] = c => c.Id,
                ["name"] = c => c.Name,
                ["type"] = c => c.Type,
                ["totalFriends"] = c => c.TotalFriends,
                ["createdTime"] = c => c.CreatedTime
            };
            query = query.ApplyOrdering(queryObj, columnsMap);

            result.TotalItems = await query.CountAsync();

            query = query.ApplyPaging(queryObj);

            result.Items = await query.ToListAsync();

            return result;
        }

Solution

  • I solved it, I will first explain my assumption, since the fix might be very much limited to my scenario.

    I have all of my DBContext used in 3 repositories. They all use async functions however they all contain awaits inside for any of the async functions used inside of them. The issue seemed to only occur once I started using these repositories as before I was accessing the dbContext directly in the Controller. This made me consider the problems in the link, which I also posted a picture of in the question:

    https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1

    Even though it specified middle ware only, I assumed it was worth a chance since I couldn't figure any other problem.

    Now as for the actual problem. One of my functions in the UserRepository, GetUser() is an async method, and even though the error seemed to be in the FriendRepository methods, since they were always the ones crashing, it turns out that the GetUser() function was used once in startup under AddJwtBearer without await.

    I had assumed that since it had an await inside of it, it would not create a problem. I also had not noticed this was a problem since I was so focused on the other repository. My hope was that maybe I was missing something as simple as the dependency injection through a constructor in middleware switching lifetime regardless of what the lifetime was already set to.

    For anyone else in the future, I ended up doing 2 things which allowed me to clearly debug my application step by step.

    1. I created a Logger static class which allows me to save text to file easily. I use this to log functions being used, constructors etc. This let me ensure that I could track the amount of times constructors and functions were called, in what order and which ones would not be reached. Here is the Logger for anyone else:

      public static class Logger
      {
       public static void Log(string text, string fileName)
       {
           string path = System.IO.Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + "/" + fileName;
      
           bool done = false;
      
           while (!done)
           {
               done = true;
      
               try
               {
                   FileStream fileStream = null;
                   fileStream = System.IO.File.Open(path, System.IO.File.Exists(path) ? FileMode.Append : FileMode.OpenOrCreate);
      
                   using (StreamWriter fs = new StreamWriter(fileStream))
                   {
                       fs.WriteLine(text);
                   };
                   fileStream.Close();
               }
               catch (IOException)
               {
                   done = false;
      
               }
      
           }
       }
       public static void Log(string text)
       {
           Log(text, "logger.txt");
       }
      }
      
    2. I added a string to the DBContext and whenever I use it in any function I would add the name of the function after the name of the class it is used in. So if my FriendsRepository would use it in a function GetTypes, it would look like:

      myDbContext.methodUsing = "FriendsRepository>GetTypes()";
      

    Thank you to @GuruStron for being patient and giving me advice on how to take this step by step, explaining to me that the middleware error idea had no leg to stand on and suggesting to me on how to approach debugging.