Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IUserTimeZoneService to make it easier to mock UserTimeZoneService #16614

Merged
merged 19 commits into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Modules;
using OrchardCore.Users.Models;
using OrchardCore.Users.TimeZone.Services;

namespace OrchardCore.Users;

/// <summary>
/// Provides an extension for <see cref="IUserTimeZoneService"/>.
/// </summary>
public static class UserTimeZoneServiceExtensions
{
/// <summary>
/// Gets the time zone for currently logged-in user.
/// </summary>
/// <param name="userTimeZoneService">The <see cref="IUserTimeZoneService"/>.</param>
public static async ValueTask<ITimeZone> GetTimeZoneAsync(this IUserTimeZoneService userTimeZoneService)
hishamco marked this conversation as resolved.
Show resolved Hide resolved
{
ArgumentNullException.ThrowIfNull(nameof(userTimeZoneService));
hishamco marked this conversation as resolved.
Show resolved Hide resolved

var currentUser = await GetCurrentUserAsync(userTimeZoneService.HttpContext);

return await userTimeZoneService.GetTimeZoneAsync(currentUser);
}

/// <summary>
/// Updates the time zone for currently logged-in user.
/// </summary>
/// <param name="userTimeZoneService">The <see cref="IUserTimeZoneService"/>.</param>
public static async ValueTask UpdateTimeZoneAsync(this IUserTimeZoneService userTimeZoneService)
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
{
ArgumentNullException.ThrowIfNull(nameof(userTimeZoneService));
hishamco marked this conversation as resolved.
Show resolved Hide resolved

var currentUser = await GetCurrentUserAsync(userTimeZoneService.HttpContext);

await userTimeZoneService.UpdateTimeZoneAsync(currentUser);
}

private static async Task<IUser> GetCurrentUserAsync(HttpContext httpContext)
{
var userName = httpContext.User?.Identity?.Name;
var userManager = httpContext.RequestServices.GetRequiredService<UserManager<IUser>>();

return await userManager.FindByNameAsync(userName) as User;
hishamco marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ namespace OrchardCore.Users.TimeZone.Drivers;

public sealed class UserTimeZoneDisplayDriver : SectionDisplayDriver<User, UserTimeZone>
{
private readonly UserTimeZoneService _userTimeZoneService;
private readonly IUserTimeZoneService _userTimeZoneService;

public UserTimeZoneDisplayDriver(UserTimeZoneService userTimeZoneService)
public UserTimeZoneDisplayDriver(IUserTimeZoneService userTimeZoneService)
{
_userTimeZoneService = userTimeZoneService;
}
Expand All @@ -33,7 +33,7 @@ public override async Task<IDisplayResult> UpdateAsync(User user, UserTimeZone u
userTimeZone.TimeZoneId = model.TimeZoneId;

// Remove the cache entry, don't update it, as the form might still fail validation for other reasons.
await _userTimeZoneService.UpdateUserTimeZoneAsync(user);
await _userTimeZoneService.UpdateTimeZoneAsync(user);

return await EditAsync(user, userTimeZone, context);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Http;
using OrchardCore.Modules;

namespace OrchardCore.Users.TimeZone.Services;

/// <summary>
/// Contract for user time zone service.
/// </summary>
public interface IUserTimeZoneService
{
/// <summary>
/// Gets the current <see cref="HttpContext"/>.
/// </summary>
public HttpContext HttpContext { get; }
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets the time zone for the specified user.
/// </summary>
/// <param name="user">The <see cref="IUser"/>.</param>
public ValueTask<ITimeZone> GetTimeZoneAsync(IUser user);
hishamco marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Updates the time zone for the specified user.
/// </summary>
/// <param name="user">The <see cref="IUser"/>.</param>
public ValueTask UpdateTimeZoneAsync(IUser user);
hishamco marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ namespace OrchardCore.Users.TimeZone.Services;
/// </summary>
public class UserTimeZoneSelector : ITimeZoneSelector
{
private readonly UserTimeZoneService _userTimeZoneService;
private readonly IUserTimeZoneService _userTimeZoneService;

public UserTimeZoneSelector(UserTimeZoneService userTimeZoneService)
public UserTimeZoneSelector(IUserTimeZoneService userTimeZoneService)
{
_userTimeZoneService = userTimeZoneService;
}
Expand All @@ -21,7 +21,7 @@ public Task<TimeZoneSelectorResult> GetTimeZoneAsync()
{
Priority = 100,
TimeZoneId = async () =>
(await _userTimeZoneService.GetUserTimeZoneAsync())?.TimeZoneId
(await _userTimeZoneService.GetTimeZoneAsync())?.TimeZoneId
}
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,35 @@

namespace OrchardCore.Users.TimeZone.Services;

public class UserTimeZoneService
/// <summary>
/// Represents a time zone service for currently logged-in user.
/// </summary>
hishamco marked this conversation as resolved.
Show resolved Hide resolved
public class UserTimeZoneService : IUserTimeZoneService
{
private const string EmptyTimeZone = "empty";
private const string CacheKey = "UserTimeZone/";
private const string EmptyTimeZone = "empty";
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved

private static readonly DistributedCacheEntryOptions _slidingExpiration = new() { SlidingExpiration = TimeSpan.FromHours(1) };

private readonly IClock _clock;
private readonly IDistributedCache _distributedCache;
private readonly IHttpContextAccessor _httpContextAccessor;

public UserTimeZoneService(
IClock clock,
IDistributedCache distributedCache,
IHttpContextAccessor httpContextAccessor
)
IHttpContextAccessor httpContextAccessor)
hishamco marked this conversation as resolved.
Show resolved Hide resolved
{
_clock = clock;
_distributedCache = distributedCache;
_httpContextAccessor = httpContextAccessor;
HttpContext = httpContextAccessor.HttpContext;
}

public async ValueTask<ITimeZone> GetUserTimeZoneAsync()
public HttpContext HttpContext { get; }

/// <inheritdoc/>
public async ValueTask<ITimeZone> GetTimeZoneAsync(IUser user)
{
var currentTimeZoneId = await GetCurrentUserTimeZoneIdAsync();
var currentTimeZoneId = await GetTimeZoneIdAsync();
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved

if (string.IsNullOrEmpty(currentTimeZoneId))
{
Expand All @@ -42,7 +47,8 @@ public async ValueTask<ITimeZone> GetUserTimeZoneAsync()
return _clock.GetTimeZone(currentTimeZoneId);
}

public async ValueTask UpdateUserTimeZoneAsync(IUser user)
/// <inheritdoc/>
public async ValueTask UpdateTimeZoneAsync(IUser user)
{
var userName = user?.UserName;
hishamco marked this conversation as resolved.
Show resolved Hide resolved

Expand All @@ -54,23 +60,25 @@ public async ValueTask UpdateUserTimeZoneAsync(IUser user)
return;
}

public async ValueTask<string> GetCurrentUserTimeZoneIdAsync()
/// <inheritdoc/>
private async ValueTask<string> GetTimeZoneIdAsync()
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
{
var userName = _httpContextAccessor.HttpContext?.User?.Identity?.Name;
var userName = HttpContext.User?.Identity?.Name;

if (string.IsNullOrEmpty(userName))
{
return null;
}

var key = GetCacheKey(userName);

var timeZoneId = await _distributedCache.GetStringAsync(key);

// The timezone is not cached yet, resolve it and store the value
if (string.IsNullOrEmpty(timeZoneId))
{
// Delay-loading UserManager since it is registered as scoped
var userManager = _httpContextAccessor.HttpContext.RequestServices.GetRequiredService<UserManager<IUser>>();
var userManager = HttpContext.RequestServices.GetRequiredService<UserManager<IUser>>();
var user = await userManager.FindByNameAsync(userName) as User;
timeZoneId = user.As<UserTimeZone>()?.TimeZoneId;

Expand All @@ -81,11 +89,7 @@ public async ValueTask<string> GetCurrentUserTimeZoneIdAsync()
timeZoneId = EmptyTimeZone;
}

await _distributedCache.SetStringAsync(
key,
timeZoneId,
_slidingExpiration
);
await _distributedCache.SetStringAsync(key, timeZoneId, _slidingExpiration);
}

// Do we know this user doesn't have a configured value?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public sealed class Startup : StartupBase
public override void ConfigureServices(IServiceCollection services)
{
services.AddScoped<ITimeZoneSelector, UserTimeZoneSelector>();
services.AddSingleton<UserTimeZoneService>();
services.AddSingleton<IUserTimeZoneService, UserTimeZoneService>();
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
services.AddScoped<IDisplayDriver<User>, UserTimeZoneDisplayDriver>();
}
}