Basic Background
I have an ASP.NET Core MVC application that uses Client (e.g., "customer") information for every HTTP request. The collection of Clients, as well as their information, rarely changes and is stored in a database. If it does change and some subsequent requests still use the old information, or even if the application throws an exception because of the change, that is OK.
Goal
I would like my ClientService
class to cache the Client information to remove making a database call on every request. However, I believe that making this class a singleton in the DI container means that it needs to be thread-safe. (If I am wrong about this, I am still interested in whether the code below would be.) The thread-safety of this code and its conformance to best practices for thread-safety is my primary goal, although performance considerations would also be appreciated.
Code
Below is the ClientService
class, which is the primary class that I am working on. I have also included the Clone
method of the Client
class since it is referenced. ClientService
is added to the DI container as a singleton in Startup
.
One pretty significant question...
Are lock
s even needed here? This is my first time using them.
First time Code Review post- please advise of any improvements that this site would prefer. Thanks in advance for your time reading!
public class ClientService
{
private readonly IServiceProvider _serviceProvider;
private readonly string _key;
public ClientService(
IKeyProvider keyProvider,
IServiceProvider serviceProvider
)
{
_serviceProvider = serviceProvider;
_key = keyProvider.GetKey();
}
private readonly object _cachedClientsLock = new object();
private Dictionary<int, Client> _cachedClients = null;
public async Task<Client> GetClient(int clientId)
{
await EnsureCache();
lock (_cachedClientsLock)
{
return _cachedClients[clientId].Clone();
}
}
public async Task<Client> GetClientByCode(string code)
{
await EnsureCache();
lock (_cachedClientsLock)
{
foreach (Client client in _cachedClients.Values)
if (client.Code == code)
return client.Clone();
}
return null;
}
private async Task EnsureCache()
{
bool clientsAreCached;
lock (_cachedClientsLock)
{
clientsAreCached = _cachedClients != null;
}
if (!clientsAreCached)
{
Dictionary<int, Client> clients = await GetClients();
lock (_cachedClientsLock)
{
_cachedClients = clients;
}
}
}
private async Task<Dictionary<int, Client>> GetClients()
{
using (IServiceScope scope = _serviceProvider.CreateScope())
{
ConnectionStringProvider connectionStringProvider = scope.ServiceProvider.GetRequiredService<ConnectionStringProvider>();
string connectionString = await connectionStringProvider.GetConnectionString();
// SQL execution using SqlCommand and SqlDataReader
Dictionary<int, Client> results = new Dictionary<int, Client>();
// SqlDataReader populates the dictionary
// _key is used here
return results;
}
}
}
public class Client
{
// Properties
public Client Clone()
{
return new Client()
{
Id = this.Id,
Code = this.Code,
// etc. for remaining properties, which are all strings
};
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IClientService, ClientService>();
// other services and configuration
}
}
```