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 14 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
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.UpdateAsync(user);

return await EditAsync(user, userTimeZone, context);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 time zone for the specified user.
/// </summary>
/// <param name="user">The <see cref="IUser"/>.</param>
public ValueTask<ITimeZone> GetAsync(IUser user);

/// <summary>
/// Updates the time zone for the specified user.
/// </summary>
/// <param name="user">The <see cref="IUser"/>.</param>
public ValueTask UpdateAsync(IUser user);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using OrchardCore.Modules;

namespace OrchardCore.Users.TimeZone.Services;
Expand All @@ -7,22 +9,35 @@ namespace OrchardCore.Users.TimeZone.Services;
/// </summary>
public class UserTimeZoneSelector : ITimeZoneSelector
{
private readonly UserTimeZoneService _userTimeZoneService;
private readonly IUserTimeZoneService _userTimeZoneService;
private readonly UserManager<IUser> _userManager;
private readonly IHttpContextAccessor _httpContextAccessor;

public UserTimeZoneSelector(UserTimeZoneService userTimeZoneService)
public UserTimeZoneSelector(
IUserTimeZoneService userTimeZoneService,
UserManager<IUser> userManager,
IHttpContextAccessor httpContextAccessor)
{
_userTimeZoneService = userTimeZoneService;
_userManager = userManager;
_httpContextAccessor = httpContextAccessor;
}

public Task<TimeZoneSelectorResult> GetTimeZoneAsync()
public async Task<TimeZoneSelectorResult> GetTimeZoneAsync()
{
return Task.FromResult(
new TimeZoneSelectorResult
{
Priority = 100,
TimeZoneId = async () =>
(await _userTimeZoneService.GetUserTimeZoneAsync())?.TimeZoneId
}
);
var currentUser = await GetCurrentUserAsync();

return new TimeZoneSelectorResult
{
Priority = 100,
TimeZoneId = async () => (await _userTimeZoneService.GetAsync(currentUser))?.TimeZoneId
};
}

private async Task<IUser> GetCurrentUserAsync()
{
var userName = _httpContextAccessor.HttpContext.User.Identity.Name;

return await _userManager.FindByNameAsync(userName);
hishamco marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,36 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Entities;
using OrchardCore.Modules;
using OrchardCore.Users.Models;
using OrchardCore.Users.TimeZone.Models;

namespace OrchardCore.Users.TimeZone.Services;

public class UserTimeZoneService
/// <summary>
/// Represents a time zone service for a user.
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
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
)
public UserTimeZoneService(IClock clock, IDistributedCache distributedCache)
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
{
_clock = clock;
_distributedCache = distributedCache;
_httpContextAccessor = httpContextAccessor;
}

public async ValueTask<ITimeZone> GetUserTimeZoneAsync()
/// <inheritdoc/>
public async ValueTask<ITimeZone> GetAsync(IUser user)
{
var currentTimeZoneId = await GetCurrentUserTimeZoneIdAsync();
ArgumentNullException.ThrowIfNull(user);

var currentTimeZoneId = await GetTimeZoneIdAsync(user);

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

public async ValueTask UpdateUserTimeZoneAsync(IUser user)
/// <inheritdoc/>
public async ValueTask UpdateAsync(IUser user)
{
var userName = user?.UserName;
ArgumentNullException.ThrowIfNull(user);

var userName = user.UserName;
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved

if (!string.IsNullOrEmpty(userName))
{
Expand All @@ -54,25 +55,23 @@ public async ValueTask UpdateUserTimeZoneAsync(IUser user)
return;
}

public async ValueTask<string> GetCurrentUserTimeZoneIdAsync()
/// <inheritdoc/>
private async ValueTask<string> GetTimeZoneIdAsync(IUser user)
{
var userName = _httpContextAccessor.HttpContext?.User?.Identity?.Name;

var userName = user.UserName;
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 user = await userManager.FindByNameAsync(userName) as User;
timeZoneId = user.As<UserTimeZone>()?.TimeZoneId;
timeZoneId = (user as User).As<UserTimeZone>()?.TimeZoneId;
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved

// We store a special string to remember there is no specific value for this user.
// And actual distributed cache implementation might not be able to store null values.
Expand All @@ -81,11 +80,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>();
}
}
1 change: 1 addition & 0 deletions src/docs/releases/2.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ public class RegisterUserFormDisplayDriver : DisplayDriver<RegisterUserForm>

- A new `UserConfirmedEvent` workflow event is now available. This event is triggered when a user successfully confirms their email address using the link sent during user registration.

- We are introducing a new interface, `IUserTimeZoneService`, to replace the existing `UserTimeZoneService`. If your project directly injects `UserTimeZoneService`, you will need to switch to using `IUserTimeZoneService` instead.

### Notifications Module

Expand Down