I have a simple ASP.NET Core 2.2 Web API that uses Windows Authentication and requires the following:
- if a user is not yet in the database, insert it on first access
- if a user has not accessed the application in the last X hours, increment the access count for that specific user
Currently, I have written a quick and dirty solution for this:
/// <inheritdoc/>
public ValidationResult<bool> EnsureCurrentUserIsRegistered()
{
var ret = new ValidationResult<bool> { Payload = false };
string username = GetHttpContextUserName();
if (string.IsNullOrWhiteSpace(username))
return new ValidationResult<bool> { IsError = true, Message = "No logged in user" };
var user = AppUserRepository.All.FirstOrDefault(u => u.Username == username);
DateTime now = TimeService.GetCurrentUtcDateTime();
if (user != null)
{
user.IsEnabled = true;
// do not count if the last access was quite recent
if ((now - (user.LastAccessTime ?? new DateTime(2018, 1, 1))).TotalHours > 8)
user.AccessCount++;
user.LastAccessTime = now;
DataAccess.SaveChanges();
return ret;
}
// fetching A/D info to use in newly created record
var userInfoRes = ActiveDirectoryService.GetUserInfoByLogOn(username);
if (userInfoRes.IsError)
{
string msg = $"Could not find A/D info for user {username}";
Logger.LogError(msg);
}
Logger.LogInfo("Creating non-existent user {username}");
// user does not exist, creating it with minimum rights
var userInfo = userInfoRes.Payload;
var dbAppUser = new AppUser
{
Email = userInfo?.EmailAddress ?? "noemail@metrosystems.net",
FirstName = userInfo?.FirstName ?? "<no_first_name>",
LastName = userInfo?.LastName ?? "<no last name>",
IsEnabled = true,
Username = username,
UserPrincipalName = userInfo?.UserPrincipalName ?? "<no UPN>",
IsSuperUser = false,
LastAccessTime = TimeService.GetCurrentUtcDateTime(),
AccessCount = 1
};
AppUserRepository.Insert(dbAppUser);
DataAccess.SaveChanges();
ret.Payload = true;
return ret;
}
The code is called from a middleware:
/// <summary>
///
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
try
{
if (context.Request.Method == "OPTIONS")
{
await _next(context);
return;
}
ISecurityService securityService =
(ISecurityService) context.RequestServices.GetService(typeof(ISecurityService));
securityService.EnsureCurrentUserIsRegistered();
}
catch (Exception exc)
{
ILoggingService logger =
(ILoggingService)context.RequestServices.GetService(typeof(ILoggingService));
logger.LogError(exc, "Failed to ensure current user is registered");
}
await _next(context);
}
The UI is a SPA that might trigger multiple requests in the same time and since the above logic is not thread safe, it sometimes fails.
I am thinking about how to easily implement a thread safety mechanism:
- use a session to store access information
- define a ConcurrentDictionary (username => access info) and store the information there
However, this looks rather convoluted and I am wondering if there is any built in mechanism that allows some code to be executed in a critical section at user level.
Question: How to efficiently ensure thread safety for code being executed in parallel for the same client/user?