user service refactor

This commit is contained in:
Lightczx
2023-12-12 17:07:11 +08:00
parent 57f7ac944c
commit 1c991aa120
12 changed files with 269 additions and 220 deletions

View File

@@ -10,17 +10,10 @@ public class CollectionsMarshalTest
[TestMethod]
public void DictionaryMarshalGetValueRefOrNullRefIsNullRef()
{
#if NET8_0_OR_GREATER
Dictionary<uint, string> dictionaryValueKeyRefValue = [];
Dictionary<uint, uint> dictionaryValueKeyValueValue = [];
Dictionary<string, uint> dictionaryRefKeyValueValue = [];
Dictionary<string, string> dictionaryRefKeyRefValue = [];
#else
Dictionary<uint, string> dictionaryValueKeyRefValue = new();
Dictionary<uint, uint> dictionaryValueKeyValueValue = new();
Dictionary<string, uint> dictionaryRefKeyValueValue = new();
Dictionary<string, string> dictionaryRefKeyRefValue = new();
#endif
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyRefValue, 1U)));
Assert.IsTrue(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dictionaryValueKeyValueValue, 1U)));
@@ -31,17 +24,10 @@ public class CollectionsMarshalTest
[TestMethod]
public void DictionaryMarshalGetValueRefOrAddDefaultIsDefault()
{
#if NET8_0_OR_GREATER
Dictionary<uint, string> dictionaryValueKeyRefValue = [];
Dictionary<uint, uint> dictionaryValueKeyValueValue = [];
Dictionary<string, uint> dictionaryRefKeyValueValue = [];
Dictionary<string, string> dictionaryRefKeyRefValue = [];
#else
Dictionary<uint, string> dictionaryValueKeyRefValue = new();
Dictionary<uint, uint> dictionaryValueKeyValueValue = new();
Dictionary<string, uint> dictionaryRefKeyValueValue = new();
Dictionary<string, string> dictionaryRefKeyRefValue = new();
#endif
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyRefValue, 1U, out _) == default);
Assert.IsTrue(CollectionsMarshal.GetValueRefOrAddDefault(dictionaryValueKeyValueValue, 1U, out _) == default);

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -7,8 +8,6 @@ namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public sealed class JsonSerializeTest
{
public TestContext? TestContext { get; set; }
private readonly JsonSerializerOptions AlowStringNumberOptions = new()
{
NumberHandling = JsonNumberHandling.AllowReadingFromString,
@@ -36,7 +35,7 @@ public sealed class JsonSerializeTest
[TestMethod]
public void DelegatePropertyCanSerialize()
{
Sample sample = JsonSerializer.Deserialize<Sample>(SmapleObjectJson)!;
SampleDelegatePropertyClass sample = JsonSerializer.Deserialize<SampleDelegatePropertyClass>(SmapleObjectJson)!;
Assert.AreEqual(sample.B, 1);
}
@@ -44,7 +43,7 @@ public sealed class JsonSerializeTest
[ExpectedException(typeof(JsonException))]
public void EmptyStringCannotSerializeAsNumber()
{
StringNumberSample sample = JsonSerializer.Deserialize<StringNumberSample>(SmapleEmptyStringObjectJson)!;
SampleStringReadWriteNumberPropertyClass sample = JsonSerializer.Deserialize<SampleStringReadWriteNumberPropertyClass>(SmapleEmptyStringObjectJson)!;
Assert.AreEqual(sample.A, 0);
}
@@ -58,37 +57,28 @@ public sealed class JsonSerializeTest
[TestMethod]
public void ByteArraySerializeAsBase64()
{
byte[] array =
#if NET8_0_OR_GREATER
[1, 2, 3, 4, 5];
#else
{ 1, 2, 3, 4, 5 };
#endif
ByteArraySample sample = new()
SampleByteArrayPropertyClass sample = new()
{
Array = array,
Array = [1, 2, 3, 4, 5],
};
string result = JsonSerializer.Serialize(sample);
TestContext!.WriteLine($"ByteArray Serialize Result: {result}");
Assert.AreEqual(result, """
{"Array":"AQIDBAU="}
""");
Assert.AreEqual(result, """{"Array":"AQIDBAU="}""");
}
private sealed class Sample
private sealed class SampleDelegatePropertyClass
{
public int A { get => B; set => B = value; }
public int B { get; set; }
}
private sealed class StringNumberSample
private sealed class SampleStringReadWriteNumberPropertyClass
{
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
public int A { get; set; }
}
private sealed class ByteArraySample
private sealed class SampleByteArrayPropertyClass
{
public byte[]? Array { get; set; }
}

View File

@@ -9,11 +9,10 @@ public sealed class LinqTest
{
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void LinqOrderByWithWrapperStruct()
public void LinqOrderByWithWrapperStructThrow()
{
List<MyUInt32> list = [1, 5, 2, 6, 3, 7, 4, 8];
string result = string.Join(", ", list.OrderBy(i => i).Select(i => i.Value));
Console.WriteLine(result);
}

View File

@@ -159,19 +159,11 @@ public sealed class GeniusInvokationDecoding
result.CopyTo(resultArray);
ushort[] testKnownResult =
#if NET8_0_OR_GREATER
[
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
];
#else
{
060, 019, 001, 079, 120, 120, 129, 151, 151, 153, 153,
181, 184, 184, 185, 185, 194, 194, 200, 200, 201, 201,
217, 217, 219, 241, 241, 244, 244, 245, 245, 270, 270,
};
#endif
CollectionAssert.AreEqual(resultArray, testKnownResult);
}

View File

@@ -15,6 +15,7 @@ public class SpiralAbyssScheduleIdTest
DateTimeOffset dateTimeOffset = new(2020, 7, 1, 4, 0, 0, Utc8);
Console.WriteLine($"2020-07-01 04:00:00 为第 {GetForDateTimeOffset(dateTimeOffset)} 期");
}
public static int GetForDateTimeOffset(DateTimeOffset dateTimeOffset)
{
// Force time in UTC+08

View File

@@ -167,7 +167,7 @@ internal static partial class EnumerableExtension
return results;
}
public static async ValueTask<List<TResult>> SelectListAsync<TSource, TResult>(this List<TSource> list, Func<TSource, CancellationToken, ValueTask<TResult>> selector, CancellationToken token)
public static async ValueTask<List<TResult>> SelectListAsync<TSource, TResult>(this List<TSource> list, Func<TSource, CancellationToken, ValueTask<TResult>> selector, CancellationToken token = default)
{
List<TResult> results = new(list.Count);

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.ViewModel.User.User;
namespace Snap.Hutao.Service.User;
internal interface IUserCollectionService
{
BindingUser? CurrentUser { get; set; }
ValueTask<ObservableCollection<UserAndUid>> GetUserAndUidCollectionAsync();
ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync();
UserGameRole? GetUserGameRoleByUid(string uid);
ValueTask RemoveUserAsync(BindingUser user);
ValueTask<ValueResult<UserOptionResult, string>> TryCreateAndAddUserFromCookieAsync(Cookie cookie, bool isOversea);
bool TryGetUserByMid(string mid, [NotNullWhen(true)] out BindingUser? user);
}

View File

@@ -50,13 +50,6 @@ internal interface IUserService
/// <returns>处理的结果</returns>
ValueTask<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie, bool isOversea);
/// <summary>
/// 异步刷新 Cookie 的 CookieToken
/// </summary>
/// <param name="user">用户</param>
/// <returns>是否刷新成功</returns>
ValueTask<bool> RefreshCookieTokenAsync(BindingUser user);
ValueTask<bool> RefreshCookieTokenAsync(Model.Entity.User user);
/// <summary>

View File

@@ -0,0 +1,185 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Message;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using BindingUser = Snap.Hutao.ViewModel.User.User;
namespace Snap.Hutao.Service.User;
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IUserCollectionService))]
internal sealed partial class UserCollectionService : IUserCollectionService
{
private readonly ScopedDbCurrent<BindingUser, Model.Entity.User, UserChangedMessage> dbCurrent;
private readonly IUserInitializationService userInitializationService;
private readonly IUserDbService userDbService;
private readonly ITaskContext taskContext;
private readonly IMessenger messenger;
private readonly Throttler throttler = new();
private ObservableCollection<BindingUser>? userCollection;
private Dictionary<string, BindingUser>? midUserMap;
private ObservableCollection<UserAndUid>? userAndUidCollection;
private Dictionary<string, UserGameRole>? uidUserGameRoleMap;
public BindingUser? CurrentUser
{
get => dbCurrent.Current;
set => dbCurrent.Current = value;
}
public async ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync()
{
using (await throttler.ThrottleAsync().ConfigureAwait(false))
{
if (userCollection is null)
{
List<Model.Entity.User> entities = await userDbService.GetUserListAsync().ConfigureAwait(false);
List<BindingUser> users = await entities.SelectListAsync(userInitializationService.ResumeUserAsync).ConfigureAwait(false);
midUserMap = [];
foreach (BindingUser user in users)
{
if (user.Entity.Mid is not null)
{
midUserMap[user.Entity.Mid] = user;
}
if (user.NeedDbUpdateAfterResume)
{
await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false);
user.NeedDbUpdateAfterResume = false;
}
}
userCollection = users.ToObservableCollection();
try
{
CurrentUser = users.SelectedOrDefault();
}
catch (InvalidOperationException ex)
{
ThrowHelper.UserdataCorrupted(SH.ServiceUserCurrentMultiMatched, ex);
}
}
}
return userCollection;
}
public async ValueTask<ObservableCollection<UserAndUid>> GetUserAndUidCollectionAsync()
{
if (userAndUidCollection is null)
{
await taskContext.SwitchToBackgroundAsync();
ObservableCollection<BindingUser> users = await GetUserCollectionAsync().ConfigureAwait(false);
List<UserAndUid> roles = [];
uidUserGameRoleMap = [];
foreach (BindingUser user in users)
{
foreach (UserGameRole role in user.UserGameRoles)
{
roles.Add(UserAndUid.From(user.Entity, role));
uidUserGameRoleMap[role.GameUid] = role;
}
}
userAndUidCollection = roles.ToObservableCollection();
}
return userAndUidCollection;
}
public async ValueTask RemoveUserAsync(BindingUser user)
{
// Sync cache
await taskContext.SwitchToMainThreadAsync();
ArgumentNullException.ThrowIfNull(userCollection);
userCollection.Remove(user);
userAndUidCollection?.RemoveWhere(r => r.User.Mid == user.Entity.Mid);
if (user.Entity.Mid is not null)
{
midUserMap?.Remove(user.Entity.Mid);
}
// Sync database
await taskContext.SwitchToBackgroundAsync();
await userDbService.DeleteUserByIdAsync(user.Entity.InnerId).ConfigureAwait(false);
messenger.Send(new UserRemovedMessage(user.Entity));
}
public UserGameRole? GetUserGameRoleByUid(string uid)
{
if (uidUserGameRoleMap is null)
{
return default;
}
try
{
return uidUserGameRoleMap[uid];
}
catch (InvalidOperationException)
{
// Sequence contains more than one matching element
// TODO: return a specialize UserGameRole to indicate error
}
return default;
}
public bool TryGetUserByMid(string mid, [NotNullWhen(true)] out BindingUser? user)
{
ArgumentNullException.ThrowIfNull(midUserMap);
return midUserMap.TryGetValue(mid, out user);
}
public async ValueTask<ValueResult<UserOptionResult, string>> TryCreateAndAddUserFromCookieAsync(Cookie cookie, bool isOversea)
{
await taskContext.SwitchToBackgroundAsync();
BindingUser? newUser = await userInitializationService.CreateUserFromCookieOrDefaultAsync(cookie, isOversea).ConfigureAwait(false);
if (newUser is null)
{
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieRequestUserInfoFailed);
}
await GetUserCollectionAsync().ConfigureAwait(false);
ArgumentNullException.ThrowIfNull(userCollection);
// Sync cache
await taskContext.SwitchToMainThreadAsync();
userCollection.Add(newUser);
if (newUser.Entity.Mid is not null)
{
midUserMap?.Add(newUser.Entity.Mid, newUser);
}
if (userAndUidCollection is not null)
{
foreach (UserGameRole role in newUser.UserGameRoles)
{
userAndUidCollection.Add(new(newUser.Entity, role));
uidUserGameRoleMap?.Add(role.GameUid, role);
}
}
// Sync database
await taskContext.SwitchToBackgroundAsync();
await userDbService.AddUserAsync(newUser.Entity).ConfigureAwait(false);
ArgumentNullException.ThrowIfNull(newUser.UserInfo);
return new(UserOptionResult.Added, newUser.UserInfo.Uid);
}
}

View File

@@ -1,11 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.DependencyInjection.Abstraction;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Message;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Passport;
@@ -23,39 +19,20 @@ namespace Snap.Hutao.Service.User;
[Injection(InjectAs.Singleton, typeof(IUserService))]
internal sealed partial class UserService : IUserService, IUserServiceUnsafe
{
private readonly Throttler throttler = new();
private readonly ScopedDbCurrent<BindingUser, Model.Entity.User, UserChangedMessage> dbCurrent;
private readonly IUserInitializationService userInitializationService;
private readonly IUserCollectionService userCollectionService;
private readonly IServiceProvider serviceProvider;
private readonly IUserDbService userDbService;
private readonly ITaskContext taskContext;
private readonly IMessenger messenger;
private ObservableCollection<BindingUser>? userCollection;
private ObservableCollection<UserAndUid>? userAndUidCollection;
/// <inheritdoc/>
public BindingUser? Current
{
get => dbCurrent.Current;
set => dbCurrent.Current = value;
get => userCollectionService.CurrentUser;
set => userCollectionService.CurrentUser = value;
}
/// <inheritdoc/>
public async ValueTask RemoveUserAsync(BindingUser user)
public ValueTask RemoveUserAsync(BindingUser user)
{
// Sync cache
await taskContext.SwitchToMainThreadAsync();
ArgumentNullException.ThrowIfNull(userCollection);
userCollection.Remove(user);
userAndUidCollection?.RemoveWhere(r => r.User.Mid == user.Entity.Mid);
// Sync database
await taskContext.SwitchToBackgroundAsync();
await userDbService.DeleteUserByIdAsync(user.Entity.InnerId).ConfigureAwait(false);
messenger.Send(new UserRemovedMessage(user.Entity));
return userCollectionService.RemoveUserAsync(user);
}
public async ValueTask UnsafeRemoveUsersAsync()
@@ -64,76 +41,21 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
await userDbService.RemoveUsersAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync()
public ValueTask<ObservableCollection<BindingUser>> GetUserCollectionAsync()
{
using (await throttler.ThrottleAsync().ConfigureAwait(false))
{
if (userCollection is null)
{
List<Model.Entity.User> entities = await userDbService.GetUserListAsync().ConfigureAwait(false);
List<BindingUser> users = await entities.SelectListAsync(userInitializationService.ResumeUserAsync, default).ConfigureAwait(false);
foreach (BindingUser user in users)
{
if (user.NeedDbUpdateAfterResume)
{
await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false);
user.NeedDbUpdateAfterResume = false;
}
}
userCollection = users.ToObservableCollection();
try
{
Current = users.SelectedOrDefault();
}
catch (InvalidOperationException ex)
{
ThrowHelper.UserdataCorrupted(SH.ServiceUserCurrentMultiMatched, ex);
}
}
}
return userCollection;
return userCollectionService.GetUserCollectionAsync();
}
/// <inheritdoc/>
public async ValueTask<ObservableCollection<UserAndUid>> GetRoleCollectionAsync()
public ValueTask<ObservableCollection<UserAndUid>> GetRoleCollectionAsync()
{
await taskContext.SwitchToBackgroundAsync();
if (userAndUidCollection is null)
{
ObservableCollection<BindingUser> users = await GetUserCollectionAsync().ConfigureAwait(false);
userAndUidCollection = users
.SelectMany(user => user.UserGameRoles.Select(role => UserAndUid.From(user.Entity, role)))
.ToObservableCollection();
}
return userAndUidCollection;
return userCollectionService.GetUserAndUidCollectionAsync();
}
/// <inheritdoc/>
public UserGameRole? GetUserGameRoleByUid(string uid)
{
if (userCollection is not null)
{
try
{
return userCollection.SelectMany(u => u.UserGameRoles).SingleOrDefault(r => r.GameUid == uid);
}
catch (InvalidOperationException)
{
// Sequence contains more than one matching element
// TODO: return a specialize UserGameRole to indicate error
}
}
return default;
return userCollectionService.GetUserGameRoleByUid(uid);
}
/// <inheritdoc/>
public async ValueTask<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie, bool isOversea)
{
await taskContext.SwitchToBackgroundAsync();
@@ -145,33 +67,22 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
}
// 检查 mid 对应用户是否存在
ArgumentNullException.ThrowIfNull(userCollection);
if (TryGetUser(userCollection, mid, out BindingUser? user))
if (!userCollectionService.TryGetUserByMid(mid, out BindingUser? user))
{
if (cookie.TryGetSToken(isOversea, out Cookie? stoken))
{
user.SToken = stoken;
user.LToken = cookie.TryGetLToken(out Cookie? ltoken) ? ltoken : user.LToken;
user.CookieToken = cookie.TryGetCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken;
await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false);
return new(UserOptionResult.Updated, mid);
}
else
{
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoSToken);
}
return await userCollectionService.TryCreateAndAddUserFromCookieAsync(cookie, isOversea).ConfigureAwait(false);
}
else
if (!cookie.TryGetSToken(isOversea, out Cookie? stoken))
{
return await TryCreateUserAndAddAsync(cookie, isOversea).ConfigureAwait(false);
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoSToken);
}
}
/// <inheritdoc/>
public ValueTask<bool> RefreshCookieTokenAsync(BindingUser user)
{
return RefreshCookieTokenAsync(user.Entity);
user.SToken = stoken;
user.LToken = cookie.TryGetLToken(out Cookie? ltoken) ? ltoken : user.LToken;
user.CookieToken = cookie.TryGetCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken;
await userDbService.UpdateUserAsync(user.Entity).ConfigureAwait(false);
return new(UserOptionResult.Updated, mid);
}
public async ValueTask<bool> RefreshCookieTokenAsync(Model.Entity.User user)
@@ -183,65 +94,20 @@ internal sealed partial class UserService : IUserService, IUserServiceUnsafe
.GetCookieAccountInfoBySTokenAsync(user)
.ConfigureAwait(false);
if (cookieTokenResponse.IsOk())
{
string cookieToken = cookieTokenResponse.Data.CookieToken;
// Check null and create a new one to avoid System.NullReferenceException
user.CookieToken ??= new();
// Sync ui and database
user.CookieToken[Cookie.COOKIE_TOKEN] = cookieToken;
await userDbService.UpdateUserAsync(user).ConfigureAwait(false);
return true;
}
else
if (!cookieTokenResponse.IsOk())
{
return false;
}
}
private static bool TryGetUser(ObservableCollection<BindingUser> users, string mid, [NotNullWhen(true)] out BindingUser? user)
{
// TODO: System.InvalidOperationException: Sequence contains more than one matching element
user = users.SingleOrDefault(u => u.Entity.Mid == mid);
return user is not null;
}
string cookieToken = cookieTokenResponse.Data.CookieToken;
private async ValueTask<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(Cookie cookie, bool isOversea)
{
await taskContext.SwitchToBackgroundAsync();
BindingUser? newUser = await userInitializationService.CreateUserFromCookieOrDefaultAsync(cookie, isOversea).ConfigureAwait(false);
// Check null and create a new one to avoid System.NullReferenceException
user.CookieToken ??= new();
if (newUser is not null)
{
// Sync cache
if (userCollection is not null)
{
await taskContext.SwitchToMainThreadAsync();
{
userCollection.Add(newUser);
// Sync ui and database
user.CookieToken[Cookie.COOKIE_TOKEN] = cookieToken;
await userDbService.UpdateUserAsync(user).ConfigureAwait(false);
if (userAndUidCollection is not null)
{
foreach (UserGameRole role in newUser.UserGameRoles)
{
userAndUidCollection.Add(new(newUser.Entity, role));
}
}
}
}
// Sync database
await taskContext.SwitchToBackgroundAsync();
await userDbService.AddUserAsync(newUser.Entity).ConfigureAwait(false);
ArgumentNullException.ThrowIfNull(newUser.UserInfo);
return new(UserOptionResult.Added, newUser.UserInfo.Uid);
}
else
{
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieRequestUserInfoFailed);
}
return true;
}
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using BindingUser = Snap.Hutao.ViewModel.User.User;
namespace Snap.Hutao.Service.User;
internal static class UserServiceExtension
{
public static ValueTask<bool> RefreshCookieTokenAsync(this IUserService userService, BindingUser user)
{
return userService.RefreshCookieTokenAsync(user.Entity);
}
}

View File

@@ -52,14 +52,12 @@ internal sealed partial class UserViewModel : ObservableObject
get => selectedUser ??= userService.Current;
set
{
// Pre select the chosen role to avoid multiple UserChangedMessage
value?.SetSelectedUserGameRole(value.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen), false);
if (SetProperty(ref selectedUser, value))
{
userService.Current = value;
if (value is not null)
{
value.SelectedUserGameRole = value.UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen);
}
}
}
}