From 8568567e69a3b72b38b1962f2ece74a9673e3dce Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Sun, 1 Nov 2026 16:51:37 +0900 Subject: [PATCH 01/12] Add .NET 10 and NativeAOT compatibility --- .github/workflows/native-aot.yml | 35 +++ MojangAPI/Cache/JsonFileCacheManager.cs | 17 +- MojangAPI/FodyWeavers.xml | 4 - MojangAPI/FodyWeavers.xsd | 30 --- MojangAPI/HttpAction/HttpActionClient.cs | 14 +- MojangAPI/HttpAction/HttpResponseHandlers.cs | 35 ++- MojangAPI/HttpAction/JsonHttpContent.cs | 32 ++- MojangAPI/Mojang.cs | 44 ++-- MojangAPI/MojangAPI.csproj | 16 +- MojangAPI/MojangAuth.cs | 92 ++++--- MojangAPI/MojangException.cs | 8 +- MojangAPI/SecurityQuestion/QuestionFlow.cs | 7 +- MojangAPI/Serialization/MojangJson.cs | 49 ++++ MojangAPI/Serialization/MojangJsonContext.cs | 28 ++ MojangAPI/Serialization/Requests.cs | 73 ++++++ README.md | 26 ++ .../MojangAPI.AotSmoke.csproj | 18 ++ tests/MojangAPI.AotSmoke/Program.cs | 246 ++++++++++++++++++ .../MojangAPI.LegacySmoke.csproj | 13 + 19 files changed, 654 insertions(+), 133 deletions(-) create mode 100644 .github/workflows/native-aot.yml delete mode 100644 MojangAPI/FodyWeavers.xml delete mode 100644 MojangAPI/FodyWeavers.xsd create mode 100644 MojangAPI/Serialization/MojangJson.cs create mode 100644 MojangAPI/Serialization/MojangJsonContext.cs create mode 100644 MojangAPI/Serialization/Requests.cs create mode 100644 tests/MojangAPI.AotSmoke/MojangAPI.AotSmoke.csproj create mode 100644 tests/MojangAPI.AotSmoke/Program.cs create mode 100644 tests/MojangAPI.LegacySmoke/MojangAPI.LegacySmoke.csproj diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml new file mode 100644 index 0000000..035fef8 --- /dev/null +++ b/.github/workflows/native-aot.yml @@ -0,0 +1,35 @@ +name: NativeAOT + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + smoke: + strategy: + matrix: + include: + - os: ubuntu-latest + rid: linux-x64 + executable: MojangAPI.AotSmoke + - os: windows-latest + rid: win-x64 + executable: MojangAPI.AotSmoke.exe + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Build both library targets + run: dotnet build MojangAPI/MojangAPI.csproj -c Release + - name: Run netstandard2.0 compatibility checks + run: dotnet run --project tests/MojangAPI.LegacySmoke -c Release + - name: Publish NativeAOT smoke test + run: dotnet publish tests/MojangAPI.AotSmoke -c Release -r ${{ matrix.rid }} -o artifacts/aot + - name: Run native smoke test + shell: bash + run: ./artifacts/aot/${{ matrix.executable }} diff --git a/MojangAPI/Cache/JsonFileCacheManager.cs b/MojangAPI/Cache/JsonFileCacheManager.cs index 6a3ed66..4a11546 100644 --- a/MojangAPI/Cache/JsonFileCacheManager.cs +++ b/MojangAPI/Cache/JsonFileCacheManager.cs @@ -1,14 +1,23 @@ -using System.IO; +using MojangAPI.Serialization; +using System.Text.Json.Serialization.Metadata; +using System.IO; using System.Text.Json; namespace MojangAPI.Cache { public class JsonFileCacheManager : ICacheManager where T : new() { + private readonly JsonTypeInfo? typeInfo; + public string CacheFilePath { get; private set; } - public JsonFileCacheManager(string filepath) + public JsonFileCacheManager(string filepath) : this(filepath, null) + { + } + + public JsonFileCacheManager(string filepath, JsonTypeInfo? typeInfo) { + this.typeInfo = typeInfo; this.CacheFilePath = filepath; } @@ -22,7 +31,7 @@ public T ReadCache() try { string filecontent = File.ReadAllText(CacheFilePath); - return JsonSerializer.Deserialize(filecontent) ?? new T(); + return MojangJson.Deserialize(filecontent, typeInfo) ?? new T(); } catch { @@ -35,7 +44,7 @@ public void SaveCache(T obj) var dirPath = Path.GetDirectoryName(CacheFilePath); if (!string.IsNullOrEmpty(dirPath)) Directory.CreateDirectory(dirPath); - File.WriteAllText(CacheFilePath, JsonSerializer.Serialize(obj)); + File.WriteAllText(CacheFilePath, MojangJson.Serialize(obj, typeInfo)); } } } diff --git a/MojangAPI/FodyWeavers.xml b/MojangAPI/FodyWeavers.xml deleted file mode 100644 index 842efef..0000000 --- a/MojangAPI/FodyWeavers.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/MojangAPI/FodyWeavers.xsd b/MojangAPI/FodyWeavers.xsd deleted file mode 100644 index 3f3946e..0000000 --- a/MojangAPI/FodyWeavers.xsd +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/MojangAPI/HttpAction/HttpActionClient.cs b/MojangAPI/HttpAction/HttpActionClient.cs index 6126b78..0fa0b67 100644 --- a/MojangAPI/HttpAction/HttpActionClient.cs +++ b/MojangAPI/HttpAction/HttpActionClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -35,25 +35,29 @@ public static async Task SendActionAsync(this HttpClient client, HttpActio httpAction.RequestHeaders.AddToHeader(httpAction.Headers); HttpResponseMessage response - = await client.SendAsync(httpAction, httpCompletionOption, cancellationToken); + = await client.SendAsync(httpAction, httpCompletionOption, cancellationToken) + .ConfigureAwait(false); T result; if (response.IsSuccessStatusCode || httpAction.ErrorHandler == null) { try { - result = await httpAction.ResponseHandler(response); + result = await httpAction.ResponseHandler(response) + .ConfigureAwait(false); } catch (Exception e) { if (httpAction.ErrorHandler != null) - result = await httpAction.ErrorHandler(response, e); + result = await httpAction.ErrorHandler(response, e) + .ConfigureAwait(false); else throw; } } else - result = await httpAction.ErrorHandler(response, null); + result = await httpAction.ErrorHandler(response, null) + .ConfigureAwait(false); ActionResponse? actionResponse = result as ActionResponse; if (actionResponse != null) diff --git a/MojangAPI/HttpAction/HttpResponseHandlers.cs b/MojangAPI/HttpAction/HttpResponseHandlers.cs index 96dd998..b8dc666 100644 --- a/MojangAPI/HttpAction/HttpResponseHandlers.cs +++ b/MojangAPI/HttpAction/HttpResponseHandlers.cs @@ -1,4 +1,6 @@ -using System; +using MojangAPI.Serialization; +using System.Text.Json.Serialization.Metadata; +using System; using System.IO; using System.Linq; using System.Net.Http; @@ -63,14 +65,12 @@ public static Func> GetSuccessCodeResponseHandler> GetJsonErrorHandler() where T : new() => GetJsonErrorHandler(new T()); public static Func> GetJsonErrorHandler(T defaultObj) => + GetJsonErrorHandler(defaultObj, null); + + public static Func> GetJsonErrorHandler(T defaultObj, JsonTypeInfo? typeInfo) => async (response, ex) => { // @@ -78,10 +78,11 @@ public static Func> GetSuccessCodeResponseHandler(resBody, jsonSerializerOptions); + var obj = MojangJson.Deserialize(resBody, typeInfo); if (obj != null) return obj; } @@ -98,13 +99,16 @@ public static Func> GetSuccessCodeResponseHandler> GetJsonHandler() => + public static Func> GetJsonHandler() => GetJsonHandler(null); + + public static Func> GetJsonHandler(JsonTypeInfo? typeInfo) => async (response) => { - string res = await response.Content.ReadAsStringAsync(); + string res = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); if (string.IsNullOrEmpty(res)) res = "{}"; - return JsonSerializer.Deserialize(res, jsonSerializerOptions); + return MojangJson.Deserialize(res, typeInfo); }; public static Func> GetDefaultErrorHandler() => @@ -117,10 +121,13 @@ public static Func> GetJsonHandler() => return Task.FromResult(default(T)); }; - public static Func> GetJsonArrayHandler() => + public static Func> GetJsonArrayHandler() => GetJsonArrayHandler(null); + + public static Func> GetJsonArrayHandler(JsonTypeInfo? typeInfo) => async (response) => { - string res = await response.Content.ReadAsStringAsync(); + string res = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); if (string.IsNullOrEmpty(res)) res = "[]"; @@ -128,7 +135,7 @@ public static Func> GetJsonArrayHandler() => var root = doc.RootElement; return root.EnumerateArray() - .Select(elem => elem.Deserialize(jsonSerializerOptions)) + .Select(elem => MojangJson.Deserialize(elem.GetRawText(), typeInfo)) .ToArray(); }; } diff --git a/MojangAPI/HttpAction/JsonHttpContent.cs b/MojangAPI/HttpAction/JsonHttpContent.cs index 3127328..d06ec5c 100644 --- a/MojangAPI/HttpAction/JsonHttpContent.cs +++ b/MojangAPI/HttpAction/JsonHttpContent.cs @@ -1,6 +1,10 @@ -using System.Net.Http; +using System.Net.Http; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +#if NET10_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif namespace HttpAction { @@ -8,24 +12,50 @@ public class JsonHttpContent : StringContent { private const string DefaultMediaType = "application/json"; +#if NET10_0_OR_GREATER + [RequiresUnreferencedCode("Use Create with source-generated JSON metadata.")] + [RequiresDynamicCode("Use Create with source-generated JSON metadata.")] +#endif public JsonHttpContent(object obj) : this(obj, null) { } +#if NET10_0_OR_GREATER + [RequiresUnreferencedCode("Use Create with source-generated JSON metadata.")] + [RequiresDynamicCode("Use Create with source-generated JSON metadata.")] +#endif public JsonHttpContent(object obj, Encoding? encoding) : this(obj, encoding, DefaultMediaType) { } +#if NET10_0_OR_GREATER + [RequiresUnreferencedCode("Use Create with source-generated JSON metadata.")] + [RequiresDynamicCode("Use Create with source-generated JSON metadata.")] +#endif public JsonHttpContent(object obj, Encoding? encoding, string mediaType) : base(serialize(obj), encoding, mediaType) { } + private JsonHttpContent(string json, Encoding? encoding, string mediaType) + : base(json, encoding, mediaType) + { + } + + public static JsonHttpContent Create(T value, JsonTypeInfo typeInfo) + { + return new JsonHttpContent(JsonSerializer.Serialize(value, typeInfo), Encoding.UTF8, DefaultMediaType); + } + +#if NET10_0_OR_GREATER + [RequiresUnreferencedCode("Use Create with source-generated JSON metadata.")] + [RequiresDynamicCode("Use Create with source-generated JSON metadata.")] +#endif public static string serialize(object obj) { return JsonSerializer.Serialize(obj, new JsonSerializerOptions diff --git a/MojangAPI/Mojang.cs b/MojangAPI/Mojang.cs index 2e4b2c6..e49de11 100644 --- a/MojangAPI/Mojang.cs +++ b/MojangAPI/Mojang.cs @@ -1,4 +1,5 @@ -using HttpAction; +using MojangAPI.Serialization; +using HttpAction; using MojangAPI.Model; using System; using System.IO; @@ -62,7 +63,8 @@ public Task GetNameHistories(string uuid) => ResponseHandler = async (response) => { var handler = HttpResponseHandlers.GetJsonArrayHandler(); - return await handler.Invoke(response); + return await handler.Invoke(response) + .ConfigureAwait(false); }, ErrorHandler = MojangException.GetMojangErrorHandler() }); @@ -73,7 +75,7 @@ public Task GetUUIDs(string[] usernames) => Method = HttpMethod.Post, Host = "https://api.mojang.com", Path = "profiles/minecraft", - Content = new JsonHttpContent(usernames ?? throw new ArgumentNullException()), + Content = JsonHttpContent.Create(usernames ?? throw new ArgumentNullException(), MojangJson.GetTypeInfo()), ResponseHandler = HttpResponseHandlers.GetJsonArrayHandler(), ErrorHandler = MojangException.GetMojangErrorHandler() }); @@ -90,11 +92,12 @@ public Task GetProfileUsingUUID(string uuid) => private async Task profileResponseHandlerFromUUID(HttpResponseMessage response) { - var responseContent = await response.Content.ReadAsStringAsync(); + var responseContent = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); using var document = JsonDocument.Parse(responseContent); var root = document.RootElement; - var profile = root.Deserialize() ?? new PlayerProfile(); + var profile = JsonSerializer.Deserialize(root.GetRawText(), MojangJson.GetTypeInfo()) ?? new PlayerProfile(); try { @@ -144,11 +147,12 @@ public Task GetProfileUsingAccessToken(string accessToken) => private async Task profileResponseHandlerFromAccessToken(HttpResponseMessage response) { - string responseContent = await response.Content.ReadAsStringAsync(); + string responseContent = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); using var doc = JsonDocument.Parse(responseContent); var root = doc.RootElement; - var profile = root.Deserialize() ?? new PlayerProfile(); + var profile = JsonSerializer.Deserialize(root.GetRawText(), MojangJson.GetTypeInfo()) ?? new PlayerProfile(); try { @@ -199,7 +203,8 @@ public Task GetPlayerBlocklist(string accessToken) => ResponseHandler = async (res) => { - var resbody = await res.Content.ReadAsStringAsync(); + var resbody = await res.Content.ReadAsStringAsync() + .ConfigureAwait(false); using var doc = JsonDocument.Parse(resbody); return doc.RootElement.GetProperty("blockedProfiles") .EnumerateArray() @@ -240,7 +245,8 @@ public Task GetPlayerCertificates(string accessToken) => ResponseHandler = async (res) => { - var resbody = await res.Content.ReadAsStringAsync(); + var resbody = await res.Content.ReadAsStringAsync() + .ConfigureAwait(false); using var doc = JsonDocument.Parse(resbody); return doc.RootElement.GetProperty("status").GetString(); }, @@ -282,11 +288,11 @@ public Task ChangeSkin(string uuid, string accessToken, SkinType { "Authorization", "Bearer " + accessToken } }, - Content = new JsonHttpContent(new + Content = JsonHttpContent.Create(new SkinRequest { - variant = skinType.GetModelType(), - url = skinUrl - }), + Variant = skinType.GetModelType(), + Url = skinUrl + }, MojangJson.GetTypeInfo()), CheckValidation = (h) => { @@ -383,7 +389,8 @@ public Task GetBlockedServer() => ResponseHandler = async (response) => { - string content = await response.Content.ReadAsStringAsync(); + string content = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); return content.Split('\n').Select(x => x.Trim()).ToArray(); }, ErrorHandler = MojangException.GetMojangErrorHandler() @@ -399,10 +406,10 @@ public Task GetStatistics(string[] options) => Host = "https://api.mojang.com", Path = "orders/statistics", - Content = new JsonHttpContent(new + Content = JsonHttpContent.Create(new StatisticsRequest { - metricKeys = options - }), + MetricKeys = options + }, MojangJson.GetTypeInfo()), ResponseHandler = HttpResponseHandlers.GetJsonHandler(), ErrorHandler = MojangException.GetMojangErrorHandler() @@ -422,7 +429,8 @@ public Task CheckGameOwnership(string accessToken) => ResponseHandler = async (response) => { - string responseContent = await response.Content.ReadAsStringAsync(); + string responseContent = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); using var doc = JsonDocument.Parse(responseContent); return doc.RootElement.GetProperty("items").EnumerateArray().Any(); }, diff --git a/MojangAPI/MojangAPI.csproj b/MojangAPI/MojangAPI.csproj index 0212f76..938f7c5 100644 --- a/MojangAPI/MojangAPI.csproj +++ b/MojangAPI/MojangAPI.csproj @@ -1,9 +1,9 @@  - netstandard2.0 + netstandard2.0;net10.0 enable - 8.0 + 10.0 true .NET Library for Mojang API, Mojang Authentication and Microsoft Xbox Authentication MIT @@ -12,11 +12,11 @@ mojang minecraft api - - - all - - - + + true + + + + diff --git a/MojangAPI/MojangAuth.cs b/MojangAPI/MojangAuth.cs index 9de7ebb..396e3a6 100644 --- a/MojangAPI/MojangAuth.cs +++ b/MojangAPI/MojangAuth.cs @@ -1,4 +1,5 @@ -using HttpAction; +using MojangAPI.Serialization; +using HttpAction; using MojangAPI.Cache; using MojangAPI.Model; using System; @@ -52,7 +53,8 @@ private async Task Request(HttpAction httpAction) where T : MojangAuthR if (string.IsNullOrEmpty(httpAction.Host)) httpAction.Host = "https://authserver.mojang.com"; - T response = await client.SendActionAsync(httpAction); + T response = await client.SendActionAsync(httpAction) + .ConfigureAwait(false); if (response.IsSuccess && response.Session != null) SaveSession(response.Session); @@ -62,7 +64,8 @@ private async Task Request(HttpAction httpAction) where T : MojangAuthR private async Task authResponseHandler(HttpResponseMessage response) { - string content = await response.Content.ReadAsStringAsync(); + string content = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); using var doc = JsonDocument.Parse(content); var root = doc.RootElement; @@ -87,8 +90,9 @@ private async Task errorResponseHandler(HttpResponseMessage? try { - string content = await response.Content.ReadAsStringAsync(); - MojangAuthResponse authResponse = JsonSerializer.Deserialize(content) + string content = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); + MojangAuthResponse authResponse = JsonSerializer.Deserialize(content, MojangJson.GetTypeInfo()) ?? new MojangAuthResponse(MojangAuthResult.UnknownError); switch (authResponse.Error) @@ -125,17 +129,12 @@ public Task Authenticate(string email, string password, stri Method = HttpMethod.Post, Path = "authenticate", - Content = new JsonHttpContent(new + Content = JsonHttpContent.Create(new AuthenticationRequest { - agent = new - { - name = "Minecraft", - version = 1 - }, - username = email, - password, - clientToken - }), + Username = email, + Password = password, + ClientToken = clientToken + }, MojangJson.GetTypeInfo()), CheckValidation = (h) => { @@ -153,7 +152,8 @@ public async Task TryAutoLogin() if (cachedSession == null || cachedSession.IsEmpty) return new MojangAuthResponse(MojangAuthResult.InvalidSession); else - return await TryAutoLogin(cachedSession); + return await TryAutoLogin(cachedSession) + .ConfigureAwait(false); } public async Task TryAutoLogin(Session session) @@ -161,9 +161,11 @@ public async Task TryAutoLogin(Session session) if (session.IsEmpty) return new MojangAuthResponse(MojangAuthResult.InvalidSession); - MojangAuthResponse res = await Validate(session); + MojangAuthResponse res = await Validate(session) + .ConfigureAwait(false); if (!res.IsSuccess) - res = await Refresh(session); + res = await Refresh(session) + .ConfigureAwait(false); return res; } @@ -183,17 +185,17 @@ public Task Refresh(Session session) => Method = HttpMethod.Post, Path = "refresh", - Content = new JsonHttpContent(new + Content = JsonHttpContent.Create(new TokenRequest { - accessToken = session.AccessToken, - clientToken = session.ClientToken, + AccessToken = session.AccessToken, + ClientToken = session.ClientToken, - selectedProfile = new + SelectedProfile = new SelectedProfile { - id = session.UUID, - name = session.Username + Id = session.UUID, + Name = session.Username } - }), + }, MojangJson.GetTypeInfo()), CheckValidation = (h) => { @@ -219,7 +221,8 @@ public async Task Validate(Session session) if (session.AccessToken == null) throw new ArgumentException("session.AccessToken"); - MojangAuthResponse res = await Validate(session.AccessToken, session.ClientToken); + MojangAuthResponse res = await Validate(session.AccessToken, session.ClientToken) + .ConfigureAwait(false); if (res.IsSuccess) res.Session = session; return res; @@ -231,11 +234,11 @@ public Task Validate(string accessToken, string? clientToken Method = HttpMethod.Post, Path = "validate", - Content = new JsonHttpContent(new + Content = JsonHttpContent.Create(new TokenRequest { - accessToken, - clientToken - }), + AccessToken = accessToken, + ClientToken = clientToken + }, MojangJson.GetTypeInfo()), CheckValidation = (h) => { @@ -253,11 +256,11 @@ public Task Signout(string email, string password) => Method = HttpMethod.Post, Path = "signout", - Content = new JsonHttpContent(new + Content = JsonHttpContent.Create(new CredentialsRequest { - username = email, - password = password - }), + Username = email, + Password = password + }, MojangJson.GetTypeInfo()), CheckValidation = (h) => { @@ -285,11 +288,11 @@ public Task Invalidate(string accessToken, string? clientTok Method = HttpMethod.Post, Path = "invalidate", - Content = new JsonHttpContent(new + Content = JsonHttpContent.Create(new TokenRequest { - accessToken = accessToken, - clientToken = clientToken - }), + AccessToken = accessToken, + ClientToken = clientToken + }, MojangJson.GetTypeInfo()), CheckValidation = (h) => { @@ -313,8 +316,9 @@ public Task LoginWithXbox(string uhs, string xstsToken) = ResponseHandler = async (response) => { - string content = await response.Content.ReadAsStringAsync(); - MicrosoftAuthResponse authResponse = JsonSerializer.Deserialize(content) + string content = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); + MicrosoftAuthResponse authResponse = JsonSerializer.Deserialize(content, MojangJson.GetTypeInfo()) ?? new MicrosoftAuthResponse(); authResponse.ExpiresOn = DateTime.Now.AddSeconds(authResponse.ExpiresIn); return authResponse; @@ -333,17 +337,19 @@ public async Task RequestSessionWithXbox(string uhs, string { try { - MicrosoftAuthResponse authResponse = await LoginWithXbox(uhs, xstsToken); + MicrosoftAuthResponse authResponse = await LoginWithXbox(uhs, xstsToken) + .ConfigureAwait(false); if (!authResponse.IsSuccess) return new MojangAuthResponse(MojangAuthResult.InvalidCredentials); Mojang mojang = new Mojang(this.client); - if (!await mojang.CheckGameOwnership(authResponse.AccessToken!)) + if (!await mojang.CheckGameOwnership(authResponse.AccessToken!).ConfigureAwait(false)) return new MojangAuthResponse(MojangAuthResult.NoProfile); - PlayerProfile profile = await mojang.GetProfileUsingAccessToken(authResponse.AccessToken!); + PlayerProfile profile = await mojang.GetProfileUsingAccessToken(authResponse.AccessToken!) + .ConfigureAwait(false); Session session = new Session { Username = profile.Name, diff --git a/MojangAPI/MojangException.cs b/MojangAPI/MojangException.cs index 34d1540..9de5bf6 100644 --- a/MojangAPI/MojangException.cs +++ b/MojangAPI/MojangException.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using System.Net.Http; @@ -69,7 +69,8 @@ public static MojangException ParseFromResponse(string responseBody, int statusC { try { - var resBody = await response.Content.ReadAsStringAsync(); + var resBody = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); if (!string.IsNullOrEmpty(resBody)) ex = ParseFromResponse(resBody, (int)response.StatusCode); } @@ -88,7 +89,8 @@ public static MojangException ParseFromResponse(string responseBody, int statusC public static Func> GetMojangErrorHandler() => async (response, ex) => { - await GetMojangErrorHandler().Invoke(response, ex); + await GetMojangErrorHandler().Invoke(response, ex) + .ConfigureAwait(false); return default(T); }; } diff --git a/MojangAPI/SecurityQuestion/QuestionFlow.cs b/MojangAPI/SecurityQuestion/QuestionFlow.cs index 8b14089..f4eba9c 100644 --- a/MojangAPI/SecurityQuestion/QuestionFlow.cs +++ b/MojangAPI/SecurityQuestion/QuestionFlow.cs @@ -1,4 +1,4 @@ -using MojangAPI.Model; +using MojangAPI.Model; using System; using System.Collections.Generic; using System.Linq; @@ -55,7 +55,8 @@ public Task GetQuestionList(string accessToken) => ResponseHandler = async (response) => { - string content = await response.Content.ReadAsStringAsync(); + string content = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); using var doc = JsonDocument.Parse(content); List questions = new List(3); @@ -86,7 +87,7 @@ public Task SendAnswers(QuestionList list, string accessToken) var jarr = new JsonArray(); foreach (var item in list) { - jarr.Add(new JsonObject + jarr.Add((JsonNode)new JsonObject { ["id"] = item.AnswerId, ["answer"] = item.Answer diff --git a/MojangAPI/Serialization/MojangJson.cs b/MojangAPI/Serialization/MojangJson.cs new file mode 100644 index 0000000..2567f37 --- /dev/null +++ b/MojangAPI/Serialization/MojangJson.cs @@ -0,0 +1,49 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace MojangAPI.Serialization +{ + internal static class MojangJson + { + internal static readonly JsonSerializerOptions Options = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + internal static T? Deserialize(string json, JsonTypeInfo? typeInfo = null) + { +#if NET10_0_OR_GREATER + return JsonSerializer.Deserialize(json, typeInfo ?? GetTypeInfo()); +#else + if (typeInfo != null) + return JsonSerializer.Deserialize(json, typeInfo); + + return JsonSerializer.Deserialize(json, Options); +#endif + } + + internal static string Serialize(T value, JsonTypeInfo? typeInfo = null) + { +#if NET10_0_OR_GREATER + return JsonSerializer.Serialize(value, typeInfo ?? GetTypeInfo()); +#else + if (typeInfo != null) + return JsonSerializer.Serialize(value, typeInfo); + + return JsonSerializer.Serialize(value); +#endif + } + + internal static JsonTypeInfo GetTypeInfo() + { + // 호출자가 정의한 형식은 생성된 메타데이터를 직접 전달해야 한다. + var typeInfo = MojangJsonContext.Default.GetTypeInfo(typeof(T)) as JsonTypeInfo; + if (typeInfo == null) + throw new NotSupportedException($"Supply a source-generated JsonTypeInfo<{typeof(T).Name}> for custom JSON types."); + + return typeInfo; + } + } +} diff --git a/MojangAPI/Serialization/MojangJsonContext.cs b/MojangAPI/Serialization/MojangJsonContext.cs new file mode 100644 index 0000000..6ce64e2 --- /dev/null +++ b/MojangAPI/Serialization/MojangJsonContext.cs @@ -0,0 +1,28 @@ +using MojangAPI.Model; +using System.Text.Json.Serialization; + +namespace MojangAPI.Serialization +{ + [JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] + [JsonSerializable(typeof(PlayerUUID))] + [JsonSerializable(typeof(PlayerUUID[]))] + [JsonSerializable(typeof(PlayerProfile))] + [JsonSerializable(typeof(NameHistory))] + [JsonSerializable(typeof(NameHistory[]))] + [JsonSerializable(typeof(PlayerAttributes))] + [JsonSerializable(typeof(PlayerCertificates))] + [JsonSerializable(typeof(Statistics))] + [JsonSerializable(typeof(Session))] + [JsonSerializable(typeof(MojangAuthResponse))] + [JsonSerializable(typeof(MicrosoftAuthResponse))] + [JsonSerializable(typeof(SkinRequest))] + [JsonSerializable(typeof(StatisticsRequest))] + [JsonSerializable(typeof(AuthenticationRequest))] + [JsonSerializable(typeof(TokenRequest))] + [JsonSerializable(typeof(CredentialsRequest))] + [JsonSerializable(typeof(string[]))] + [JsonSerializable(typeof(bool))] + internal partial class MojangJsonContext : JsonSerializerContext + { + } +} diff --git a/MojangAPI/Serialization/Requests.cs b/MojangAPI/Serialization/Requests.cs new file mode 100644 index 0000000..f02690a --- /dev/null +++ b/MojangAPI/Serialization/Requests.cs @@ -0,0 +1,73 @@ +using System.Text.Json.Serialization; + +namespace MojangAPI.Serialization +{ + internal sealed class SkinRequest + { + [JsonPropertyName("variant")] + public string? Variant { get; set; } + + [JsonPropertyName("url")] + public string? Url { get; set; } + } + + internal sealed class StatisticsRequest + { + [JsonPropertyName("metricKeys")] + public string[]? MetricKeys { get; set; } + } + + internal sealed class CredentialsRequest + { + [JsonPropertyName("username")] + public string? Username { get; set; } + + [JsonPropertyName("password")] + public string? Password { get; set; } + } + + internal sealed class AuthenticationRequest + { + [JsonPropertyName("agent")] + public Agent Agent { get; set; } = new Agent(); + + [JsonPropertyName("username")] + public string? Username { get; set; } + + [JsonPropertyName("password")] + public string? Password { get; set; } + + [JsonPropertyName("clientToken")] + public string? ClientToken { get; set; } + } + + internal sealed class Agent + { + [JsonPropertyName("name")] + public string Name { get; set; } = "Minecraft"; + + [JsonPropertyName("version")] + public int Version { get; set; } = 1; + } + + internal sealed class TokenRequest + { + [JsonPropertyName("accessToken")] + public string? AccessToken { get; set; } + + [JsonPropertyName("clientToken")] + public string? ClientToken { get; set; } + + [JsonPropertyName("selectedProfile")] + public SelectedProfile? SelectedProfile { get; set; } + } + + internal sealed class SelectedProfile + { + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + } +} diff --git a/README.md b/README.md index b65caf3..6f7217d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Support: - netstandard 2.0 +- .NET 10 (NativeAOT compatible) ## Install @@ -22,3 +23,28 @@ Install Nuget package [MojangAPI](https://www.nuget.org/packages/MojangAPI) [Documentation](https://cmllib.github.io/CmlLib.Core-wiki/en/mojangapi/) [한국어](https://cmllib.github.io/CmlLib.Core-wiki/ko/mojangapi/) + +## .NET 10 및 NativeAOT + +.NET 10 대상은 내장 JSON 메타데이터를 소스 생성하므로, JSON 리플렉션을 비활성화한 NativeAOT 앱에서도 스킨 변경·업로드, 프로필 조회, 인증 및 세션 캐시를 사용할 수 있습니다. 기존 `netstandard2.0` 대상도 유지합니다. 기존 인증 서버의 서비스 제공 여부는 이 호환성 변경의 범위에 포함되지 않습니다. + +사용자 정의 형식을 `HttpAction`의 JSON 도우미나 `JsonFileCacheManager`와 함께 사용할 때는 앱의 `JsonSerializerContext`에서 생성한 `JsonTypeInfo`를 전달해야 합니다. + +```csharp +var content = JsonHttpContent.Create(payload, AppJsonContext.Default.MyPayload); +var handler = HttpResponseHandlers.GetJsonHandler(AppJsonContext.Default.MyPayload); +var cache = new JsonFileCacheManager("cache.json", AppJsonContext.Default.MyPayload); +``` + +오류 응답과 배열 응답에도 각각 `GetJsonErrorHandler(defaultValue, typeInfo)`, `GetJsonArrayHandler(typeInfo)`를 사용할 수 있습니다. 기존 `JsonHttpContent(object)` 생성자와 `serialize(object)`는 일반 JIT 앱과의 호환성을 위해 유지하며, .NET 10에서는 트리밍 및 동적 코드 경고를 표시합니다. NativeAOT에서는 `Create(value, typeInfo)`를 사용하세요. + +오프라인 검증은 실제 계정이나 네트워크 요청 없이 실행됩니다. JSON 리플렉션을 끄고 라이브러리 전체를 트리밍 분석 대상으로 지정하며, AOT 및 트리밍 경고는 오류로 처리합니다. + +```sh +dotnet build MojangAPI/MojangAPI.csproj -c Release +dotnet run --project tests/MojangAPI.LegacySmoke -c Release +dotnet publish tests/MojangAPI.AotSmoke -c Release -r win-x64 -o artifacts/aot +./artifacts/aot/MojangAPI.AotSmoke.exe +``` + +Linux에서는 RID를 `linux-x64`로 바꾸고 확장자 없는 실행 파일을 실행하세요. 네이티브 빌드 도구가 필요하며, CI에서 Windows와 Linux의 게시 및 실행을 검증합니다. diff --git a/tests/MojangAPI.AotSmoke/MojangAPI.AotSmoke.csproj b/tests/MojangAPI.AotSmoke/MojangAPI.AotSmoke.csproj new file mode 100644 index 0000000..6f65ec7 --- /dev/null +++ b/tests/MojangAPI.AotSmoke/MojangAPI.AotSmoke.csproj @@ -0,0 +1,18 @@ + + + Exe + net10.0 + enable + enable + true + false + false + true + true + $(WarningsAsErrors);IL2026;IL3050 + + + + + + diff --git a/tests/MojangAPI.AotSmoke/Program.cs b/tests/MojangAPI.AotSmoke/Program.cs new file mode 100644 index 0000000..205f69e --- /dev/null +++ b/tests/MojangAPI.AotSmoke/Program.cs @@ -0,0 +1,246 @@ +using HttpAction; +using MojangAPI; +using MojangAPI.Cache; +using MojangAPI.Model; +using MojangAPI.SecurityQuestion; +using System.Net; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +#if LEGACY_LIBRARY +if (!JsonSerializer.IsReflectionEnabledByDefault) + throw new Exception("The legacy compatibility test requires JSON reflection."); +#else +if (JsonSerializer.IsReflectionEnabledByDefault) + throw new Exception("JSON reflection must be disabled for this test."); +#endif + +const string uuid = "853c80ef3c3749fdaa49938b674adae6"; +const string profileJson = """ + {"id":"853c80ef3c3749fdaa49938b674adae6","name":"Player","skins":[{"url":"https://example.com/skin.png","variant":"SLIM"}]} + """; +using var handler = new StubHandler(); +using var client = new HttpClient(handler); +var api = new Mojang(client); + +handler.Expect("GET", "/users/profiles/minecraft/Player", """{"id":"id","name":"Player"}"""); +Check((await api.GetUUID("Player")).CurrentUsername == "Player", "UUID response"); + +handler.Expect("POST", "/profiles/minecraft", """[{"id":"id","name":"Player"}]""", body => +{ + Check(body.RootElement[0].GetString() == "Player", "UUID array request"); +}); +Check((await api.GetUUIDs(new[] { "Player" }))[0].UUID == "id", "UUID array response"); + +handler.Expect("GET", $"/user/profiles/{uuid}/names", """[{"name":"Old","changedToAt":123}]"""); +Check((await api.GetNameHistories(uuid))[0].ChangedToAt == 123, "Name history"); + +handler.Expect("POST", "/minecraft/profile/skins", profileJson, body => +{ + Check(body.RootElement.GetProperty("variant").GetString() == "slim", "Skin variant"); + Check(body.RootElement.GetProperty("url").GetString() == "https://example.com/skin.png", "Skin URL"); +}); +var profile = await api.ChangeSkin( + uuid, + "token", + SkinType.Alex, + "https://example.com/skin.png"); +Check(profile.Name == "Player" && profile.Skin?.Model == SkinType.Alex, "Skin response"); + +handler.ExpectUpload(); +using var skin = new MemoryStream(Encoding.UTF8.GetBytes("skin-bytes")); +await api.UploadSkin( + "token", + SkinType.Steve, + skin, + "skin.png"); + +handler.Expect("DELETE", "/minecraft/profile/skins/active", ""); +await api.ResetSkin(uuid, "token"); + +handler.Expect("GET", "/minecraft/profile", profileJson); +Check((await api.GetProfileUsingAccessToken("token")).Skin?.Url == "https://example.com/skin.png", "Token profile"); + +var texture = Convert.ToBase64String(Encoding.UTF8.GetBytes(""" + {"textures":{"SKIN":{"url":"https://example.com/skin.png","metadata":{"model":"slim"}}}} + """)); +handler.Expect("GET", $"/session/minecraft/profile/{uuid}", + $$"""{"id":"{{uuid}}","name":"Player","properties":[{"value":"{{texture}}"}]}"""); +Check((await api.GetProfileUsingUUID(uuid)).Skin?.Model == SkinType.Alex, "Session profile"); + +handler.Expect("GET", "/player/attributes", """ + {"privileges":{"onlineChat":{"enabled":true}},"banStatus":{"bannedScopes":{"MULTIPLAYER":{"banId":"ban"}}}} + """); +var attributes = await api.GetPlayerAttributes("token"); +Check(attributes.Privileges?.OnlineChat?.Enabled == true, "Nested privileges"); +Check(attributes.BanStatus?.BannedScopes?["MULTIPLAYER"]?.BanId == "ban", "Ban dictionary"); + +handler.Expect("POST", "/player/certificates", """{"keyPair":{"publicKey":"key"}}"""); +Check((await api.GetPlayerCertificates("token")).KeyPair?.PublicKey == "key", "Certificates"); + +handler.Expect("POST", "/orders/statistics", """{"Total":42}""", body => +{ + Check(body.RootElement.GetProperty("metricKeys")[0].GetString() == "item_sold_minecraft", "Statistics request"); +}); +Check((await api.GetStatistics(StatisticOption.ItemSoldMinecraft)).Total == 42, "Statistics response"); + +handler.Expect("GET", "/users/profiles/minecraft/missing", """{"error":"NotFound","errorMessage":"missing"}""", status: HttpStatusCode.NotFound); +try +{ + await api.GetUUID("missing"); + throw new Exception("Expected MojangException"); +} +catch (MojangException error) +{ + Check(error.StatusCode == 404 && error.Error == "NotFound", "Error response"); +} + +var auth = new MojangAuth(client, null); +handler.Expect("POST", "/authenticate", """{"accessToken":"token","selectedProfile":{"id":"id","name":"Player"}}""", body => +{ + Check(body.RootElement.GetProperty("agent").GetProperty("name").GetString() == "Minecraft", "Authentication agent"); + Check(!body.RootElement.TryGetProperty("clientToken", out _), "Null request fields omitted"); +}); +Check((await auth.Authenticate("email", "password", null)).Session?.Username == "Player", "Authentication response"); + +var session = new Session { Username = "Player", UUID = "id", AccessToken = "token", ClientToken = "client" }; +handler.Expect("POST", "/refresh", """{"accessToken":"new","selectedProfile":{"id":"id","name":"Player"}}""", body => +{ + Check(body.RootElement.GetProperty("selectedProfile").GetProperty("id").GetString() == "id", "Refresh profile"); +}); +Check((await auth.Refresh(session)).Session?.AccessToken == "new", "Refresh response"); + +handler.Expect("POST", "/validate", ""); +Check((await auth.Validate(session)).IsSuccess, "Validate response"); +handler.Expect("POST", "/invalidate", ""); +Check((await auth.Invalidate("token", "client")).IsSuccess, "Invalidate response"); +handler.Expect("POST", "/signout", ""); +Check((await auth.Signout("email", "password")).IsSuccess, "Signout response"); +handler.Expect("POST", "/authenticate", """{"error":"ForbiddenOperationException","message":"bad credentials"}""", status: HttpStatusCode.Forbidden); +Check((await auth.Authenticate("email", "bad", null)).Result == MojangAuthResult.InvalidCredentials, "Authentication error"); + +#pragma warning disable CS0618 +handler.Expect("POST", "/authentication/login_with_xbox", """{"access_token":"minecraft","expires_in":3600,"roles":[]}"""); +Check((await auth.LoginWithXbox("uhs", "xsts")).AccessToken == "minecraft", "Xbox response"); +#pragma warning restore CS0618 + +var questions = new QuestionFlow(client); +handler.Expect("GET", "/user/security/challenges", """[{"question":{"id":1,"question":"Question"},"answer":{"id":2}}]"""); +var questionList = await questions.GetQuestionList("token"); +foreach (var question in questionList) + question.Answer = "Answer"; + +handler.Expect("POST", "/user/security/location", "true", body => +{ + Check(body.RootElement[0].GetProperty("answer").GetString() == "Answer", "Security answer"); +}); +Check(await questions.SendAnswers(questionList, "token"), "Security response"); + +var cachePath = Path.Combine(Path.GetTempPath(), $"mojang-aot-{Guid.NewGuid()}.json"); +try +{ + var cache = new SessionFileCacheManager(cachePath); + cache.SaveCache(session); + Check(cache.ReadCache().AccessToken == "token", "Session cache round trip"); + File.WriteAllText(cachePath, "invalid-json"); + Check(!string.IsNullOrEmpty(cache.ReadCache().ClientToken), "Corrupt cache fallback"); + + var customCache = new JsonFileCacheManager(cachePath, SmokeJsonContext.Default.CustomPayload); + customCache.SaveCache(new CustomPayload { Value = "custom" }); + Check(customCache.ReadCache().Value == "custom", "Custom cache metadata"); +} +finally +{ + File.Delete(cachePath); +} + +using var content = JsonHttpContent.Create(new CustomPayload { Value = "custom" }, SmokeJsonContext.Default.CustomPayload); +using var response = new HttpResponseMessage { Content = content }; +var custom = await HttpResponseHandlers.GetJsonHandler(SmokeJsonContext.Default.CustomPayload)(response); +Check(custom.Value == "custom", "Custom response metadata"); +var customError = await HttpResponseHandlers.GetJsonErrorHandler(new CustomPayload(), SmokeJsonContext.Default.CustomPayload)(response, null); +Check(customError.Value == "custom", "Custom error metadata"); +using var arrayResponse = new HttpResponseMessage { Content = new StringContent("""[{"Value":"array"}]""") }; +var customArray = await HttpResponseHandlers.GetJsonArrayHandler(SmokeJsonContext.Default.CustomPayload)(arrayResponse); +Check(customArray[0].Value == "array", "Custom array metadata"); + +#if LEGACY_LIBRARY +using var legacyContent = new JsonHttpContent(new { Value = "legacy" }); +using var legacyResponse = new HttpResponseMessage { Content = legacyContent }; +Check((await HttpResponseHandlers.GetJsonHandler()(legacyResponse)).Value == "legacy", "Legacy custom JSON reflection"); +Console.WriteLine("All netstandard2.0 compatibility checks passed."); +#else +Console.WriteLine("All NativeAOT smoke checks passed (JSON reflection disabled)."); +#endif + +static void Check(bool condition, string name) +{ + if (!condition) + throw new Exception($"Failed: {name}"); +} + +public sealed class CustomPayload +{ + public string? Value { get; set; } +} + +[JsonSerializable(typeof(CustomPayload))] +internal partial class SmokeJsonContext : JsonSerializerContext +{ +} + +internal sealed class StubHandler : HttpMessageHandler +{ + private Func>? next; + + public void Expect( + string method, + string path, + string response, + Action? inspect = null, + HttpStatusCode status = HttpStatusCode.OK) + { + next = async request => + { + if (request.Method.Method != method || request.RequestUri?.AbsolutePath != path) + throw new Exception($"Unexpected request: {request.Method} {request.RequestUri}"); + + if (inspect != null) + { + using var body = JsonDocument.Parse(await request.Content!.ReadAsStringAsync()); + inspect(body); + } + + return new HttpResponseMessage(status) { Content = new StringContent(response) }; + }; + } + + public void ExpectUpload() + { + next = async request => + { + if (request.Method != HttpMethod.Post || request.RequestUri?.AbsolutePath != "/minecraft/profile/skins") + throw new Exception("Unexpected upload endpoint"); + + if (request.Headers.Authorization?.ToString() != "Bearer token") + throw new Exception("Missing Minecraft token"); + + var parts = ((MultipartFormDataContent)request.Content!).ToArray(); + if (await parts[0].ReadAsStringAsync() != "classic" || await parts[1].ReadAsStringAsync() != "skin-bytes") + throw new Exception("Invalid multipart skin data"); + + if (parts[1].Headers.ContentType?.MediaType != "image/png") + throw new Exception("Invalid skin content type"); + + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") }; + }; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var expectation = next ?? throw new Exception("No request expected"); + next = null; + return expectation(request); + } +} diff --git a/tests/MojangAPI.LegacySmoke/MojangAPI.LegacySmoke.csproj b/tests/MojangAPI.LegacySmoke/MojangAPI.LegacySmoke.csproj new file mode 100644 index 0000000..c94314d --- /dev/null +++ b/tests/MojangAPI.LegacySmoke/MojangAPI.LegacySmoke.csproj @@ -0,0 +1,13 @@ + + + Exe + net10.0 + enable + enable + $(DefineConstants);LEGACY_LIBRARY + + + + + + From 94619627d9f14f7874a6b0cf3ef75f42606782ee Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Sun, 1 Nov 2026 16:55:11 +0900 Subject: [PATCH 02/12] Run NativeAOT CI on Linux only --- .github/workflows/native-aot.yml | 15 +++------------ README.md | 2 +- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml index 035fef8..a384960 100644 --- a/.github/workflows/native-aot.yml +++ b/.github/workflows/native-aot.yml @@ -9,16 +9,7 @@ permissions: jobs: smoke: - strategy: - matrix: - include: - - os: ubuntu-latest - rid: linux-x64 - executable: MojangAPI.AotSmoke - - os: windows-latest - rid: win-x64 - executable: MojangAPI.AotSmoke.exe - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 @@ -29,7 +20,7 @@ jobs: - name: Run netstandard2.0 compatibility checks run: dotnet run --project tests/MojangAPI.LegacySmoke -c Release - name: Publish NativeAOT smoke test - run: dotnet publish tests/MojangAPI.AotSmoke -c Release -r ${{ matrix.rid }} -o artifacts/aot + run: dotnet publish tests/MojangAPI.AotSmoke -c Release -r linux-x64 -o artifacts/aot - name: Run native smoke test shell: bash - run: ./artifacts/aot/${{ matrix.executable }} + run: ./artifacts/aot/MojangAPI.AotSmoke diff --git a/README.md b/README.md index 6f7217d..820ca90 100644 --- a/README.md +++ b/README.md @@ -47,4 +47,4 @@ dotnet publish tests/MojangAPI.AotSmoke -c Release -r win-x64 -o artifacts/aot ./artifacts/aot/MojangAPI.AotSmoke.exe ``` -Linux에서는 RID를 `linux-x64`로 바꾸고 확장자 없는 실행 파일을 실행하세요. 네이티브 빌드 도구가 필요하며, CI에서 Windows와 Linux의 게시 및 실행을 검증합니다. +Linux에서는 RID를 `linux-x64`로 바꾸고 확장자 없는 실행 파일을 실행하세요. 네이티브 빌드 도구가 필요하며, CI에서 Linux의 게시 및 실행을 검증합니다. From 00d4a82f04e8c8d818a35c0196672eddfab91c6d Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Sun, 1 Nov 2026 17:00:12 +0900 Subject: [PATCH 03/12] Remove smoke test projects and CI workflow --- .github/workflows/native-aot.yml | 26 -- README.md | 11 - .../MojangAPI.AotSmoke.csproj | 18 -- tests/MojangAPI.AotSmoke/Program.cs | 246 ------------------ .../MojangAPI.LegacySmoke.csproj | 13 - 5 files changed, 314 deletions(-) delete mode 100644 .github/workflows/native-aot.yml delete mode 100644 tests/MojangAPI.AotSmoke/MojangAPI.AotSmoke.csproj delete mode 100644 tests/MojangAPI.AotSmoke/Program.cs delete mode 100644 tests/MojangAPI.LegacySmoke/MojangAPI.LegacySmoke.csproj diff --git a/.github/workflows/native-aot.yml b/.github/workflows/native-aot.yml deleted file mode 100644 index a384960..0000000 --- a/.github/workflows/native-aot.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: NativeAOT - -on: - push: - pull_request: - -permissions: - contents: read - -jobs: - smoke: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 10.0.x - - name: Build both library targets - run: dotnet build MojangAPI/MojangAPI.csproj -c Release - - name: Run netstandard2.0 compatibility checks - run: dotnet run --project tests/MojangAPI.LegacySmoke -c Release - - name: Publish NativeAOT smoke test - run: dotnet publish tests/MojangAPI.AotSmoke -c Release -r linux-x64 -o artifacts/aot - - name: Run native smoke test - shell: bash - run: ./artifacts/aot/MojangAPI.AotSmoke diff --git a/README.md b/README.md index 820ca90..f3ebc2b 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,3 @@ var cache = new JsonFileCacheManager("cache.json", AppJsonContext.Def ``` 오류 응답과 배열 응답에도 각각 `GetJsonErrorHandler(defaultValue, typeInfo)`, `GetJsonArrayHandler(typeInfo)`를 사용할 수 있습니다. 기존 `JsonHttpContent(object)` 생성자와 `serialize(object)`는 일반 JIT 앱과의 호환성을 위해 유지하며, .NET 10에서는 트리밍 및 동적 코드 경고를 표시합니다. NativeAOT에서는 `Create(value, typeInfo)`를 사용하세요. - -오프라인 검증은 실제 계정이나 네트워크 요청 없이 실행됩니다. JSON 리플렉션을 끄고 라이브러리 전체를 트리밍 분석 대상으로 지정하며, AOT 및 트리밍 경고는 오류로 처리합니다. - -```sh -dotnet build MojangAPI/MojangAPI.csproj -c Release -dotnet run --project tests/MojangAPI.LegacySmoke -c Release -dotnet publish tests/MojangAPI.AotSmoke -c Release -r win-x64 -o artifacts/aot -./artifacts/aot/MojangAPI.AotSmoke.exe -``` - -Linux에서는 RID를 `linux-x64`로 바꾸고 확장자 없는 실행 파일을 실행하세요. 네이티브 빌드 도구가 필요하며, CI에서 Linux의 게시 및 실행을 검증합니다. diff --git a/tests/MojangAPI.AotSmoke/MojangAPI.AotSmoke.csproj b/tests/MojangAPI.AotSmoke/MojangAPI.AotSmoke.csproj deleted file mode 100644 index 6f65ec7..0000000 --- a/tests/MojangAPI.AotSmoke/MojangAPI.AotSmoke.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - Exe - net10.0 - enable - enable - true - false - false - true - true - $(WarningsAsErrors);IL2026;IL3050 - - - - - - diff --git a/tests/MojangAPI.AotSmoke/Program.cs b/tests/MojangAPI.AotSmoke/Program.cs deleted file mode 100644 index 205f69e..0000000 --- a/tests/MojangAPI.AotSmoke/Program.cs +++ /dev/null @@ -1,246 +0,0 @@ -using HttpAction; -using MojangAPI; -using MojangAPI.Cache; -using MojangAPI.Model; -using MojangAPI.SecurityQuestion; -using System.Net; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; - -#if LEGACY_LIBRARY -if (!JsonSerializer.IsReflectionEnabledByDefault) - throw new Exception("The legacy compatibility test requires JSON reflection."); -#else -if (JsonSerializer.IsReflectionEnabledByDefault) - throw new Exception("JSON reflection must be disabled for this test."); -#endif - -const string uuid = "853c80ef3c3749fdaa49938b674adae6"; -const string profileJson = """ - {"id":"853c80ef3c3749fdaa49938b674adae6","name":"Player","skins":[{"url":"https://example.com/skin.png","variant":"SLIM"}]} - """; -using var handler = new StubHandler(); -using var client = new HttpClient(handler); -var api = new Mojang(client); - -handler.Expect("GET", "/users/profiles/minecraft/Player", """{"id":"id","name":"Player"}"""); -Check((await api.GetUUID("Player")).CurrentUsername == "Player", "UUID response"); - -handler.Expect("POST", "/profiles/minecraft", """[{"id":"id","name":"Player"}]""", body => -{ - Check(body.RootElement[0].GetString() == "Player", "UUID array request"); -}); -Check((await api.GetUUIDs(new[] { "Player" }))[0].UUID == "id", "UUID array response"); - -handler.Expect("GET", $"/user/profiles/{uuid}/names", """[{"name":"Old","changedToAt":123}]"""); -Check((await api.GetNameHistories(uuid))[0].ChangedToAt == 123, "Name history"); - -handler.Expect("POST", "/minecraft/profile/skins", profileJson, body => -{ - Check(body.RootElement.GetProperty("variant").GetString() == "slim", "Skin variant"); - Check(body.RootElement.GetProperty("url").GetString() == "https://example.com/skin.png", "Skin URL"); -}); -var profile = await api.ChangeSkin( - uuid, - "token", - SkinType.Alex, - "https://example.com/skin.png"); -Check(profile.Name == "Player" && profile.Skin?.Model == SkinType.Alex, "Skin response"); - -handler.ExpectUpload(); -using var skin = new MemoryStream(Encoding.UTF8.GetBytes("skin-bytes")); -await api.UploadSkin( - "token", - SkinType.Steve, - skin, - "skin.png"); - -handler.Expect("DELETE", "/minecraft/profile/skins/active", ""); -await api.ResetSkin(uuid, "token"); - -handler.Expect("GET", "/minecraft/profile", profileJson); -Check((await api.GetProfileUsingAccessToken("token")).Skin?.Url == "https://example.com/skin.png", "Token profile"); - -var texture = Convert.ToBase64String(Encoding.UTF8.GetBytes(""" - {"textures":{"SKIN":{"url":"https://example.com/skin.png","metadata":{"model":"slim"}}}} - """)); -handler.Expect("GET", $"/session/minecraft/profile/{uuid}", - $$"""{"id":"{{uuid}}","name":"Player","properties":[{"value":"{{texture}}"}]}"""); -Check((await api.GetProfileUsingUUID(uuid)).Skin?.Model == SkinType.Alex, "Session profile"); - -handler.Expect("GET", "/player/attributes", """ - {"privileges":{"onlineChat":{"enabled":true}},"banStatus":{"bannedScopes":{"MULTIPLAYER":{"banId":"ban"}}}} - """); -var attributes = await api.GetPlayerAttributes("token"); -Check(attributes.Privileges?.OnlineChat?.Enabled == true, "Nested privileges"); -Check(attributes.BanStatus?.BannedScopes?["MULTIPLAYER"]?.BanId == "ban", "Ban dictionary"); - -handler.Expect("POST", "/player/certificates", """{"keyPair":{"publicKey":"key"}}"""); -Check((await api.GetPlayerCertificates("token")).KeyPair?.PublicKey == "key", "Certificates"); - -handler.Expect("POST", "/orders/statistics", """{"Total":42}""", body => -{ - Check(body.RootElement.GetProperty("metricKeys")[0].GetString() == "item_sold_minecraft", "Statistics request"); -}); -Check((await api.GetStatistics(StatisticOption.ItemSoldMinecraft)).Total == 42, "Statistics response"); - -handler.Expect("GET", "/users/profiles/minecraft/missing", """{"error":"NotFound","errorMessage":"missing"}""", status: HttpStatusCode.NotFound); -try -{ - await api.GetUUID("missing"); - throw new Exception("Expected MojangException"); -} -catch (MojangException error) -{ - Check(error.StatusCode == 404 && error.Error == "NotFound", "Error response"); -} - -var auth = new MojangAuth(client, null); -handler.Expect("POST", "/authenticate", """{"accessToken":"token","selectedProfile":{"id":"id","name":"Player"}}""", body => -{ - Check(body.RootElement.GetProperty("agent").GetProperty("name").GetString() == "Minecraft", "Authentication agent"); - Check(!body.RootElement.TryGetProperty("clientToken", out _), "Null request fields omitted"); -}); -Check((await auth.Authenticate("email", "password", null)).Session?.Username == "Player", "Authentication response"); - -var session = new Session { Username = "Player", UUID = "id", AccessToken = "token", ClientToken = "client" }; -handler.Expect("POST", "/refresh", """{"accessToken":"new","selectedProfile":{"id":"id","name":"Player"}}""", body => -{ - Check(body.RootElement.GetProperty("selectedProfile").GetProperty("id").GetString() == "id", "Refresh profile"); -}); -Check((await auth.Refresh(session)).Session?.AccessToken == "new", "Refresh response"); - -handler.Expect("POST", "/validate", ""); -Check((await auth.Validate(session)).IsSuccess, "Validate response"); -handler.Expect("POST", "/invalidate", ""); -Check((await auth.Invalidate("token", "client")).IsSuccess, "Invalidate response"); -handler.Expect("POST", "/signout", ""); -Check((await auth.Signout("email", "password")).IsSuccess, "Signout response"); -handler.Expect("POST", "/authenticate", """{"error":"ForbiddenOperationException","message":"bad credentials"}""", status: HttpStatusCode.Forbidden); -Check((await auth.Authenticate("email", "bad", null)).Result == MojangAuthResult.InvalidCredentials, "Authentication error"); - -#pragma warning disable CS0618 -handler.Expect("POST", "/authentication/login_with_xbox", """{"access_token":"minecraft","expires_in":3600,"roles":[]}"""); -Check((await auth.LoginWithXbox("uhs", "xsts")).AccessToken == "minecraft", "Xbox response"); -#pragma warning restore CS0618 - -var questions = new QuestionFlow(client); -handler.Expect("GET", "/user/security/challenges", """[{"question":{"id":1,"question":"Question"},"answer":{"id":2}}]"""); -var questionList = await questions.GetQuestionList("token"); -foreach (var question in questionList) - question.Answer = "Answer"; - -handler.Expect("POST", "/user/security/location", "true", body => -{ - Check(body.RootElement[0].GetProperty("answer").GetString() == "Answer", "Security answer"); -}); -Check(await questions.SendAnswers(questionList, "token"), "Security response"); - -var cachePath = Path.Combine(Path.GetTempPath(), $"mojang-aot-{Guid.NewGuid()}.json"); -try -{ - var cache = new SessionFileCacheManager(cachePath); - cache.SaveCache(session); - Check(cache.ReadCache().AccessToken == "token", "Session cache round trip"); - File.WriteAllText(cachePath, "invalid-json"); - Check(!string.IsNullOrEmpty(cache.ReadCache().ClientToken), "Corrupt cache fallback"); - - var customCache = new JsonFileCacheManager(cachePath, SmokeJsonContext.Default.CustomPayload); - customCache.SaveCache(new CustomPayload { Value = "custom" }); - Check(customCache.ReadCache().Value == "custom", "Custom cache metadata"); -} -finally -{ - File.Delete(cachePath); -} - -using var content = JsonHttpContent.Create(new CustomPayload { Value = "custom" }, SmokeJsonContext.Default.CustomPayload); -using var response = new HttpResponseMessage { Content = content }; -var custom = await HttpResponseHandlers.GetJsonHandler(SmokeJsonContext.Default.CustomPayload)(response); -Check(custom.Value == "custom", "Custom response metadata"); -var customError = await HttpResponseHandlers.GetJsonErrorHandler(new CustomPayload(), SmokeJsonContext.Default.CustomPayload)(response, null); -Check(customError.Value == "custom", "Custom error metadata"); -using var arrayResponse = new HttpResponseMessage { Content = new StringContent("""[{"Value":"array"}]""") }; -var customArray = await HttpResponseHandlers.GetJsonArrayHandler(SmokeJsonContext.Default.CustomPayload)(arrayResponse); -Check(customArray[0].Value == "array", "Custom array metadata"); - -#if LEGACY_LIBRARY -using var legacyContent = new JsonHttpContent(new { Value = "legacy" }); -using var legacyResponse = new HttpResponseMessage { Content = legacyContent }; -Check((await HttpResponseHandlers.GetJsonHandler()(legacyResponse)).Value == "legacy", "Legacy custom JSON reflection"); -Console.WriteLine("All netstandard2.0 compatibility checks passed."); -#else -Console.WriteLine("All NativeAOT smoke checks passed (JSON reflection disabled)."); -#endif - -static void Check(bool condition, string name) -{ - if (!condition) - throw new Exception($"Failed: {name}"); -} - -public sealed class CustomPayload -{ - public string? Value { get; set; } -} - -[JsonSerializable(typeof(CustomPayload))] -internal partial class SmokeJsonContext : JsonSerializerContext -{ -} - -internal sealed class StubHandler : HttpMessageHandler -{ - private Func>? next; - - public void Expect( - string method, - string path, - string response, - Action? inspect = null, - HttpStatusCode status = HttpStatusCode.OK) - { - next = async request => - { - if (request.Method.Method != method || request.RequestUri?.AbsolutePath != path) - throw new Exception($"Unexpected request: {request.Method} {request.RequestUri}"); - - if (inspect != null) - { - using var body = JsonDocument.Parse(await request.Content!.ReadAsStringAsync()); - inspect(body); - } - - return new HttpResponseMessage(status) { Content = new StringContent(response) }; - }; - } - - public void ExpectUpload() - { - next = async request => - { - if (request.Method != HttpMethod.Post || request.RequestUri?.AbsolutePath != "/minecraft/profile/skins") - throw new Exception("Unexpected upload endpoint"); - - if (request.Headers.Authorization?.ToString() != "Bearer token") - throw new Exception("Missing Minecraft token"); - - var parts = ((MultipartFormDataContent)request.Content!).ToArray(); - if (await parts[0].ReadAsStringAsync() != "classic" || await parts[1].ReadAsStringAsync() != "skin-bytes") - throw new Exception("Invalid multipart skin data"); - - if (parts[1].Headers.ContentType?.MediaType != "image/png") - throw new Exception("Invalid skin content type"); - - return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}") }; - }; - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var expectation = next ?? throw new Exception("No request expected"); - next = null; - return expectation(request); - } -} diff --git a/tests/MojangAPI.LegacySmoke/MojangAPI.LegacySmoke.csproj b/tests/MojangAPI.LegacySmoke/MojangAPI.LegacySmoke.csproj deleted file mode 100644 index c94314d..0000000 --- a/tests/MojangAPI.LegacySmoke/MojangAPI.LegacySmoke.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - Exe - net10.0 - enable - enable - $(DefineConstants);LEGACY_LIBRARY - - - - - - From c3a406dfd2eb487cab06af8fc0a36ac18452216f Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Sun, 1 Nov 2026 17:00:57 +0900 Subject: [PATCH 04/12] Restore original README --- README.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/README.md b/README.md index f3ebc2b..b65caf3 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ Support: - netstandard 2.0 -- .NET 10 (NativeAOT compatible) ## Install @@ -23,17 +22,3 @@ Install Nuget package [MojangAPI](https://www.nuget.org/packages/MojangAPI) [Documentation](https://cmllib.github.io/CmlLib.Core-wiki/en/mojangapi/) [한국어](https://cmllib.github.io/CmlLib.Core-wiki/ko/mojangapi/) - -## .NET 10 및 NativeAOT - -.NET 10 대상은 내장 JSON 메타데이터를 소스 생성하므로, JSON 리플렉션을 비활성화한 NativeAOT 앱에서도 스킨 변경·업로드, 프로필 조회, 인증 및 세션 캐시를 사용할 수 있습니다. 기존 `netstandard2.0` 대상도 유지합니다. 기존 인증 서버의 서비스 제공 여부는 이 호환성 변경의 범위에 포함되지 않습니다. - -사용자 정의 형식을 `HttpAction`의 JSON 도우미나 `JsonFileCacheManager`와 함께 사용할 때는 앱의 `JsonSerializerContext`에서 생성한 `JsonTypeInfo`를 전달해야 합니다. - -```csharp -var content = JsonHttpContent.Create(payload, AppJsonContext.Default.MyPayload); -var handler = HttpResponseHandlers.GetJsonHandler(AppJsonContext.Default.MyPayload); -var cache = new JsonFileCacheManager("cache.json", AppJsonContext.Default.MyPayload); -``` - -오류 응답과 배열 응답에도 각각 `GetJsonErrorHandler(defaultValue, typeInfo)`, `GetJsonArrayHandler(typeInfo)`를 사용할 수 있습니다. 기존 `JsonHttpContent(object)` 생성자와 `serialize(object)`는 일반 JIT 앱과의 호환성을 위해 유지하며, .NET 10에서는 트리밍 및 동적 코드 경고를 표시합니다. NativeAOT에서는 `Create(value, typeInfo)`를 사용하세요. From 93eb7442c1a3eec5341fb3d78d765a828649f9ca Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Sun, 1 Nov 2026 17:08:06 +0900 Subject: [PATCH 05/12] Remove legacy authentication session caching --- MojangAPI/Cache/ICacheManager.cs | 9 -- MojangAPI/Cache/JsonFileCacheManager.cs | 50 ----------- MojangAPI/Cache/SessionFileCacheManager.cs | 21 ----- MojangAPI/MojangAuth.cs | 92 +++----------------- MojangAPI/Serialization/MojangJson.cs | 12 --- MojangAPI/Serialization/MojangJsonContext.cs | 1 - MojangAPISample/TestMojangAuth.cs | 17 +--- docs/MojangAuth.md | 53 +++-------- 8 files changed, 26 insertions(+), 229 deletions(-) delete mode 100644 MojangAPI/Cache/ICacheManager.cs delete mode 100644 MojangAPI/Cache/JsonFileCacheManager.cs delete mode 100644 MojangAPI/Cache/SessionFileCacheManager.cs diff --git a/MojangAPI/Cache/ICacheManager.cs b/MojangAPI/Cache/ICacheManager.cs deleted file mode 100644 index 79b2d1d..0000000 --- a/MojangAPI/Cache/ICacheManager.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MojangAPI.Cache -{ - public interface ICacheManager - { - T GetDefaultObject(); - T ReadCache(); - void SaveCache(T obj); - } -} diff --git a/MojangAPI/Cache/JsonFileCacheManager.cs b/MojangAPI/Cache/JsonFileCacheManager.cs deleted file mode 100644 index 4a11546..0000000 --- a/MojangAPI/Cache/JsonFileCacheManager.cs +++ /dev/null @@ -1,50 +0,0 @@ -using MojangAPI.Serialization; -using System.Text.Json.Serialization.Metadata; -using System.IO; -using System.Text.Json; - -namespace MojangAPI.Cache -{ - public class JsonFileCacheManager : ICacheManager where T : new() - { - private readonly JsonTypeInfo? typeInfo; - - public string CacheFilePath { get; private set; } - - public JsonFileCacheManager(string filepath) : this(filepath, null) - { - } - - public JsonFileCacheManager(string filepath, JsonTypeInfo? typeInfo) - { - this.typeInfo = typeInfo; - this.CacheFilePath = filepath; - } - - public virtual T GetDefaultObject() => new T(); - - public T ReadCache() - { - if (!File.Exists(CacheFilePath)) - return GetDefaultObject(); - - try - { - string filecontent = File.ReadAllText(CacheFilePath); - return MojangJson.Deserialize(filecontent, typeInfo) ?? new T(); - } - catch - { - return GetDefaultObject(); - } - } - - public void SaveCache(T obj) - { - var dirPath = Path.GetDirectoryName(CacheFilePath); - if (!string.IsNullOrEmpty(dirPath)) - Directory.CreateDirectory(dirPath); - File.WriteAllText(CacheFilePath, MojangJson.Serialize(obj, typeInfo)); - } - } -} diff --git a/MojangAPI/Cache/SessionFileCacheManager.cs b/MojangAPI/Cache/SessionFileCacheManager.cs deleted file mode 100644 index ec2e7b5..0000000 --- a/MojangAPI/Cache/SessionFileCacheManager.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MojangAPI.Model; -using System; - -namespace MojangAPI.Cache -{ - public class SessionFileCacheManager : JsonFileCacheManager - { - public SessionFileCacheManager(string filepath) : base (filepath) - { - - } - - public override Session GetDefaultObject() - { - return new Session - { - ClientToken = Guid.NewGuid().ToString() - }; - } - } -} diff --git a/MojangAPI/MojangAuth.cs b/MojangAPI/MojangAuth.cs index 396e3a6..eab2b69 100644 --- a/MojangAPI/MojangAuth.cs +++ b/MojangAPI/MojangAuth.cs @@ -1,6 +1,5 @@ using MojangAPI.Serialization; using HttpAction; -using MojangAPI.Cache; using MojangAPI.Model; using System; using System.Net.Http; @@ -12,54 +11,25 @@ namespace MojangAPI public class MojangAuth { private readonly HttpClient client; - private readonly ICacheManager? cacheManager; - public MojangAuth() : this(null) { } - - public MojangAuth(HttpClient? client) : - this(client, new SessionFileCacheManager("mojang_auth.json")) - { } - - public MojangAuth(HttpClient? httpClient, ICacheManager? _cacheManager) + public MojangAuth() : this(null) { - if (httpClient == null) - this.client = Mojang.DefaultClient.Value; - else - this.client = httpClient; - - this.cacheManager = _cacheManager; } - private string CreateClientToken() + public MojangAuth(HttpClient? client) { - return Guid.NewGuid().ToString(); - } - - private void SaveSession(Session session) - { - if (string.IsNullOrEmpty(session.ClientToken)) - { - Session? cachedSession = cacheManager?.ReadCache(); - if (cachedSession == null || string.IsNullOrEmpty(cachedSession.ClientToken)) - session.ClientToken = CreateClientToken(); - else - session.ClientToken = cachedSession.ClientToken; - } - cacheManager?.SaveCache(session); + if (client == null) + this.client = Mojang.DefaultClient.Value; + else + this.client = client; } - private async Task Request(HttpAction httpAction) where T : MojangAuthResponse + private Task Request(HttpAction httpAction) where T : MojangAuthResponse { if (string.IsNullOrEmpty(httpAction.Host)) httpAction.Host = "https://authserver.mojang.com"; - T response = await client.SendActionAsync(httpAction) - .ConfigureAwait(false); - - if (response.IsSuccess && response.Session != null) - SaveSession(response.Session); - - return response; + return client.SendActionAsync(httpAction); } private async Task authResponseHandler(HttpResponseMessage response) @@ -72,11 +42,16 @@ private async Task authResponseHandler(HttpResponseMessage r if (!root.TryGetProperty("selectedProfile", out var selectedProfile)) return new MojangAuthResponse(MojangAuthResult.NoProfile); + string? clientToken = null; + if (root.TryGetProperty("clientToken", out var clientTokenProperty)) + clientToken = clientTokenProperty.GetString(); + return new MojangAuthResponse(MojangAuthResult.Success) { Session = new Session { AccessToken = root.GetProperty("accessToken").GetString(), + ClientToken = clientToken, UUID = selectedProfile.GetProperty("id").GetString(), Username = selectedProfile.GetProperty("name").GetString() } @@ -121,7 +96,7 @@ private async Task errorResponseHandler(HttpResponseMessage? } public Task Authenticate(string email, string password) => - Authenticate(email, password, cacheManager?.ReadCache()?.ClientToken); + Authenticate(email, password, Guid.NewGuid().ToString()); public Task Authenticate(string email, string password, string? clientToken) => Request(new HttpAction @@ -146,16 +121,6 @@ public Task Authenticate(string email, string password, stri ErrorHandler = errorResponseHandler }); - public async Task TryAutoLogin() - { - Session? cachedSession = cacheManager?.ReadCache(); - if (cachedSession == null || cachedSession.IsEmpty) - return new MojangAuthResponse(MojangAuthResult.InvalidSession); - else - return await TryAutoLogin(cachedSession) - .ConfigureAwait(false); - } - public async Task TryAutoLogin(Session session) { if (session.IsEmpty) @@ -170,15 +135,6 @@ public async Task TryAutoLogin(Session session) return res; } - public Task Refresh() - { - Session? cachedSession = cacheManager?.ReadCache(); - if (cachedSession == null) - return Task.FromResult(new MojangAuthResponse(MojangAuthResult.InvalidSession)); - else - return Refresh(cachedSession); - } - public Task Refresh(Session session) => Request(new HttpAction { @@ -207,15 +163,6 @@ public Task Refresh(Session session) => ErrorHandler = errorResponseHandler }); - public Task Validate() - { - Session? cachedSession = cacheManager?.ReadCache(); - if (cachedSession == null || string.IsNullOrEmpty(cachedSession.AccessToken)) - return Task.FromResult(new MojangAuthResponse(MojangAuthResult.InvalidSession)); - else - return Validate(cachedSession); - } - public async Task Validate(Session session) { if (session.AccessToken == null) @@ -273,15 +220,6 @@ public Task Signout(string email, string password) => ErrorHandler = errorResponseHandler }); - public Task Invalidate() - { - Session? cachedSession = cacheManager?.ReadCache(); - if (cachedSession == null || cachedSession.AccessToken == null) - return Task.FromResult(new MojangAuthResponse(MojangAuthResult.InvalidSession)); - else - return Invalidate(cachedSession.AccessToken, cachedSession.ClientToken); - } - public Task Invalidate(string accessToken, string? clientToken) => Request(new HttpAction { @@ -357,8 +295,6 @@ public async Task RequestSessionWithXbox(string uhs, string UUID = profile.UUID }; - SaveSession(session); - return new MojangAuthResponse(MojangAuthResult.Success) { Session = session diff --git a/MojangAPI/Serialization/MojangJson.cs b/MojangAPI/Serialization/MojangJson.cs index 2567f37..50c76d7 100644 --- a/MojangAPI/Serialization/MojangJson.cs +++ b/MojangAPI/Serialization/MojangJson.cs @@ -24,18 +24,6 @@ internal static class MojangJson #endif } - internal static string Serialize(T value, JsonTypeInfo? typeInfo = null) - { -#if NET10_0_OR_GREATER - return JsonSerializer.Serialize(value, typeInfo ?? GetTypeInfo()); -#else - if (typeInfo != null) - return JsonSerializer.Serialize(value, typeInfo); - - return JsonSerializer.Serialize(value); -#endif - } - internal static JsonTypeInfo GetTypeInfo() { // 호출자가 정의한 형식은 생성된 메타데이터를 직접 전달해야 한다. diff --git a/MojangAPI/Serialization/MojangJsonContext.cs b/MojangAPI/Serialization/MojangJsonContext.cs index 6ce64e2..7bde5a9 100644 --- a/MojangAPI/Serialization/MojangJsonContext.cs +++ b/MojangAPI/Serialization/MojangJsonContext.cs @@ -12,7 +12,6 @@ namespace MojangAPI.Serialization [JsonSerializable(typeof(PlayerAttributes))] [JsonSerializable(typeof(PlayerCertificates))] [JsonSerializable(typeof(Statistics))] - [JsonSerializable(typeof(Session))] [JsonSerializable(typeof(MojangAuthResponse))] [JsonSerializable(typeof(MicrosoftAuthResponse))] [JsonSerializable(typeof(SkinRequest))] diff --git a/MojangAPISample/TestMojangAuth.cs b/MojangAPISample/TestMojangAuth.cs index 914a690..8596b19 100644 --- a/MojangAPISample/TestMojangAuth.cs +++ b/MojangAPISample/TestMojangAuth.cs @@ -13,28 +13,13 @@ internal class TestMojangAuth { public static async Task Test(MojangAuth auth) { - MojangAuthResponse res; - - Console.WriteLine("TryAutoLogin"); - res = await auth.TryAutoLogin(); - printAuthResponse(res); - Console.WriteLine(); - - if (res.IsSuccess) - return new MSession - { - AccessToken = res.Session.AccessToken, - UUID = res.Session.UUID, - Username = res.Session.Username - }; - Console.WriteLine("Authenticate"); Console.Write("Email : "); var email = Console.ReadLine(); Console.Write("Password : "); var pw = Console.ReadLine(); - res = await auth.Authenticate(email, pw); // fill your mojang email and password + var res = await auth.Authenticate(email, pw); printAuthResponse(res); Console.WriteLine(); diff --git a/docs/MojangAuth.md b/docs/MojangAuth.md index 1d3cb62..832f1a4 100644 --- a/docs/MojangAuth.md +++ b/docs/MojangAuth.md @@ -15,7 +15,7 @@ You can check whether the request was successful or failed to check `IsSuccess` `Session` property is the result of authenticate. It contains `Username`, `AccessToken`, and `UUID` If `IsSuccess` is false, `Result`, `Error`, and `ErrorMessage` property tell you why the request failed. -`MojangAuth` has its own session caching manager. You don't have to type mojang email and password everytime when you login. Login once using your mojang email and password, and `MojangAuth` will save your session information into json file(not contain your raw password, it contains only tokens). Next time when you login just call `TryAutoLogin`. it authenticate you using cached session without typing mojang email and password. +`MojangAuth`는 세션을 저장하지 않습니다. 인증 결과의 `Session`은 호출자가 보관하고, 재인증 시 `TryAutoLogin(session)`, `Refresh(session)`, `Validate(session)`에 직접 전달해야 합니다. There are two ways to login. One is `Yggdrasil Mojang Authentication` which is described below, and the other is `Microsoft Xbox Authentication` which is new way to login. [Microsoft Xbox Authentication](./XboxAuthentication.md) @@ -26,16 +26,7 @@ Example: HttpClient httpClient = new HttpClient(); MojangAuth auth = new MojangAuth(httpClient); -MojangAuthResponse res; - -res = await auth.TryAutoLogin(); // login using cached session -if (!res.IsSuccess) // failed to login using cached session -{ - Console.WriteLine(res.Result.ToString()); - - // it will save your login information into json file - res = await auth.Authenticate("mojang email", "password"); -} +MojangAuthResponse res = await auth.Authenticate("mojang email", "password"); Console.WriteLine(res.ToString()); @@ -49,27 +40,17 @@ Session session = res.Session; #### public MojangAuth(HttpClient client) -Same as `new MojangAuth(client, new SessionFileCacheManager("mojang_auth.json"))` - -#### public MojangAuth(HttpClient httpClient, ICacheManager\ _cacheManager) - -Initialize a new instance of the `MojangAuth` class with the specified cache manager. -If `_cacheManager` is null, `MojangAuth` does not cache session. -If you want to change cache file path, create a instance of `SessionFileCacheManager` with file path. -Example: -```csharp -MojangAuth auth = new MojangAuth(client, new SessionFileCacheManager("session_file_path.json")); -``` +지정한 `HttpClient`로 인증 요청을 전송합니다. `null`을 전달하면 기본 클라이언트를 사용합니다. 세션 파일은 생성하거나 읽지 않습니다. ### Methods #### Authenticate ```csharp -// using cached client token or create new client token if it was null. +// 호출마다 새로운 클라이언트 토큰을 생성한다. MojangAuthResponse res = await auth.Authenticate("mojang email", "password"); -// using the specified client token +// 지정한 클라이언트 토큰을 사용한다. MojangAuthResponse res = await auth.Authenticate("mojang email", "password", "clientToken"); if (res.IsSuccess) @@ -87,29 +68,21 @@ else #### TryAutoLogin ```csharp -// call Validate() and Refersh() using cached session. -MojangAuthResponse res = await auth.TryAutoLogin(); +// 전달한 세션을 검증하고, 검증에 실패하면 갱신한다. +MojangAuthResponse res = await auth.TryAutoLogin(session); ``` #### Refresh ```csharp -// using cached session -MojangAuthResponse res = await auth.Refresh(); -``` -```csharp -// using the specified session +// 호출자가 보관한 세션 또는 토큰을 전달한다. MojangAuthResponse res = await auth.Refresh(session); ``` #### Validate ```csharp -// using cached session -MojangAuthResponse res = await auth.Validate(); -``` -```csharp -// using the specified session +// 호출자가 보관한 세션 또는 토큰을 전달한다. MojangAuthResponse res = await auth.Validate(session); ``` @@ -122,11 +95,7 @@ MojangAuthResponse res = await auth.Signout("mojang email", "password"); #### Invalidate ```csharp -// using cached session -MojangAuthResponse res = await auth.Invalidate(); -``` -```csharp -// using the specified session +// 호출자가 보관한 세션 또는 토큰을 전달한다. MojangAuthResponse res = await auth.Invalidate("accessToken", "clientToken"); ``` -*note: 'clientToken' can be null* \ No newline at end of file +*note: 'clientToken' can be null* From e24eb232236b77f4d27c994c4a63dd4dfc9bff81 Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Sun, 1 Nov 2026 17:10:45 +0900 Subject: [PATCH 06/12] Update ConfigureAwait.Fody and Fody for .NET 10 --- MojangAPI/FodyWeavers.xml | 4 +++ MojangAPI/FodyWeavers.xsd | 30 ++++++++++++++++++++ MojangAPI/HttpAction/HttpActionClient.cs | 14 ++++----- MojangAPI/HttpAction/HttpResponseHandlers.cs | 9 ++---- MojangAPI/Mojang.cs | 21 +++++--------- MojangAPI/MojangAPI.csproj | 5 ++++ MojangAPI/MojangAuth.cs | 26 ++++++----------- MojangAPI/MojangException.cs | 8 ++---- MojangAPI/SecurityQuestion/QuestionFlow.cs | 3 +- 9 files changed, 67 insertions(+), 53 deletions(-) create mode 100644 MojangAPI/FodyWeavers.xml create mode 100644 MojangAPI/FodyWeavers.xsd diff --git a/MojangAPI/FodyWeavers.xml b/MojangAPI/FodyWeavers.xml new file mode 100644 index 0000000..842efef --- /dev/null +++ b/MojangAPI/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/MojangAPI/FodyWeavers.xsd b/MojangAPI/FodyWeavers.xsd new file mode 100644 index 0000000..3f3946e --- /dev/null +++ b/MojangAPI/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/MojangAPI/HttpAction/HttpActionClient.cs b/MojangAPI/HttpAction/HttpActionClient.cs index 0fa0b67..6126b78 100644 --- a/MojangAPI/HttpAction/HttpActionClient.cs +++ b/MojangAPI/HttpAction/HttpActionClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -35,29 +35,25 @@ public static async Task SendActionAsync(this HttpClient client, HttpActio httpAction.RequestHeaders.AddToHeader(httpAction.Headers); HttpResponseMessage response - = await client.SendAsync(httpAction, httpCompletionOption, cancellationToken) - .ConfigureAwait(false); + = await client.SendAsync(httpAction, httpCompletionOption, cancellationToken); T result; if (response.IsSuccessStatusCode || httpAction.ErrorHandler == null) { try { - result = await httpAction.ResponseHandler(response) - .ConfigureAwait(false); + result = await httpAction.ResponseHandler(response); } catch (Exception e) { if (httpAction.ErrorHandler != null) - result = await httpAction.ErrorHandler(response, e) - .ConfigureAwait(false); + result = await httpAction.ErrorHandler(response, e); else throw; } } else - result = await httpAction.ErrorHandler(response, null) - .ConfigureAwait(false); + result = await httpAction.ErrorHandler(response, null); ActionResponse? actionResponse = result as ActionResponse; if (actionResponse != null) diff --git a/MojangAPI/HttpAction/HttpResponseHandlers.cs b/MojangAPI/HttpAction/HttpResponseHandlers.cs index b8dc666..f64d2a0 100644 --- a/MojangAPI/HttpAction/HttpResponseHandlers.cs +++ b/MojangAPI/HttpAction/HttpResponseHandlers.cs @@ -78,8 +78,7 @@ public static Func> GetSuccessCodeResponseHandler(resBody, typeInfo); @@ -104,8 +103,7 @@ public static Func> GetSuccessCodeResponseHandler> GetJsonHandler(JsonTypeInfo? typeInfo) => async (response) => { - string res = await response.Content.ReadAsStringAsync() - .ConfigureAwait(false); + string res = await response.Content.ReadAsStringAsync(); if (string.IsNullOrEmpty(res)) res = "{}"; return MojangJson.Deserialize(res, typeInfo); @@ -126,8 +124,7 @@ public static Func> GetJsonHandler(JsonTypeInfo< public static Func> GetJsonArrayHandler(JsonTypeInfo? typeInfo) => async (response) => { - string res = await response.Content.ReadAsStringAsync() - .ConfigureAwait(false); + string res = await response.Content.ReadAsStringAsync(); if (string.IsNullOrEmpty(res)) res = "[]"; diff --git a/MojangAPI/Mojang.cs b/MojangAPI/Mojang.cs index e49de11..c2c6a9c 100644 --- a/MojangAPI/Mojang.cs +++ b/MojangAPI/Mojang.cs @@ -63,8 +63,7 @@ public Task GetNameHistories(string uuid) => ResponseHandler = async (response) => { var handler = HttpResponseHandlers.GetJsonArrayHandler(); - return await handler.Invoke(response) - .ConfigureAwait(false); + return await handler.Invoke(response); }, ErrorHandler = MojangException.GetMojangErrorHandler() }); @@ -92,8 +91,7 @@ public Task GetProfileUsingUUID(string uuid) => private async Task profileResponseHandlerFromUUID(HttpResponseMessage response) { - var responseContent = await response.Content.ReadAsStringAsync() - .ConfigureAwait(false); + var responseContent = await response.Content.ReadAsStringAsync(); using var document = JsonDocument.Parse(responseContent); var root = document.RootElement; @@ -147,8 +145,7 @@ public Task GetProfileUsingAccessToken(string accessToken) => private async Task profileResponseHandlerFromAccessToken(HttpResponseMessage response) { - string responseContent = await response.Content.ReadAsStringAsync() - .ConfigureAwait(false); + string responseContent = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(responseContent); var root = doc.RootElement; @@ -203,8 +200,7 @@ public Task GetPlayerBlocklist(string accessToken) => ResponseHandler = async (res) => { - var resbody = await res.Content.ReadAsStringAsync() - .ConfigureAwait(false); + var resbody = await res.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(resbody); return doc.RootElement.GetProperty("blockedProfiles") .EnumerateArray() @@ -245,8 +241,7 @@ public Task GetPlayerCertificates(string accessToken) => ResponseHandler = async (res) => { - var resbody = await res.Content.ReadAsStringAsync() - .ConfigureAwait(false); + var resbody = await res.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(resbody); return doc.RootElement.GetProperty("status").GetString(); }, @@ -389,8 +384,7 @@ public Task GetBlockedServer() => ResponseHandler = async (response) => { - string content = await response.Content.ReadAsStringAsync() - .ConfigureAwait(false); + string content = await response.Content.ReadAsStringAsync(); return content.Split('\n').Select(x => x.Trim()).ToArray(); }, ErrorHandler = MojangException.GetMojangErrorHandler() @@ -429,8 +423,7 @@ public Task CheckGameOwnership(string accessToken) => ResponseHandler = async (response) => { - string responseContent = await response.Content.ReadAsStringAsync() - .ConfigureAwait(false); + string responseContent = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(responseContent); return doc.RootElement.GetProperty("items").EnumerateArray().Any(); }, diff --git a/MojangAPI/MojangAPI.csproj b/MojangAPI/MojangAPI.csproj index 938f7c5..64f50a8 100644 --- a/MojangAPI/MojangAPI.csproj +++ b/MojangAPI/MojangAPI.csproj @@ -16,6 +16,11 @@ true + + + + + diff --git a/MojangAPI/MojangAuth.cs b/MojangAPI/MojangAuth.cs index eab2b69..6db3c61 100644 --- a/MojangAPI/MojangAuth.cs +++ b/MojangAPI/MojangAuth.cs @@ -34,8 +34,7 @@ private Task Request(HttpAction httpAction) where T : MojangAuthRespons private async Task authResponseHandler(HttpResponseMessage response) { - string content = await response.Content.ReadAsStringAsync() - .ConfigureAwait(false); + string content = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(content); var root = doc.RootElement; @@ -65,8 +64,7 @@ private async Task errorResponseHandler(HttpResponseMessage? try { - string content = await response.Content.ReadAsStringAsync() - .ConfigureAwait(false); + string content = await response.Content.ReadAsStringAsync(); MojangAuthResponse authResponse = JsonSerializer.Deserialize(content, MojangJson.GetTypeInfo()) ?? new MojangAuthResponse(MojangAuthResult.UnknownError); @@ -126,11 +124,9 @@ public async Task TryAutoLogin(Session session) if (session.IsEmpty) return new MojangAuthResponse(MojangAuthResult.InvalidSession); - MojangAuthResponse res = await Validate(session) - .ConfigureAwait(false); + MojangAuthResponse res = await Validate(session); if (!res.IsSuccess) - res = await Refresh(session) - .ConfigureAwait(false); + res = await Refresh(session); return res; } @@ -168,8 +164,7 @@ public async Task Validate(Session session) if (session.AccessToken == null) throw new ArgumentException("session.AccessToken"); - MojangAuthResponse res = await Validate(session.AccessToken, session.ClientToken) - .ConfigureAwait(false); + MojangAuthResponse res = await Validate(session.AccessToken, session.ClientToken); if (res.IsSuccess) res.Session = session; return res; @@ -254,8 +249,7 @@ public Task LoginWithXbox(string uhs, string xstsToken) = ResponseHandler = async (response) => { - string content = await response.Content.ReadAsStringAsync() - .ConfigureAwait(false); + string content = await response.Content.ReadAsStringAsync(); MicrosoftAuthResponse authResponse = JsonSerializer.Deserialize(content, MojangJson.GetTypeInfo()) ?? new MicrosoftAuthResponse(); authResponse.ExpiresOn = DateTime.Now.AddSeconds(authResponse.ExpiresIn); @@ -275,19 +269,17 @@ public async Task RequestSessionWithXbox(string uhs, string { try { - MicrosoftAuthResponse authResponse = await LoginWithXbox(uhs, xstsToken) - .ConfigureAwait(false); + MicrosoftAuthResponse authResponse = await LoginWithXbox(uhs, xstsToken); if (!authResponse.IsSuccess) return new MojangAuthResponse(MojangAuthResult.InvalidCredentials); Mojang mojang = new Mojang(this.client); - if (!await mojang.CheckGameOwnership(authResponse.AccessToken!).ConfigureAwait(false)) + if (!await mojang.CheckGameOwnership(authResponse.AccessToken!)) return new MojangAuthResponse(MojangAuthResult.NoProfile); - PlayerProfile profile = await mojang.GetProfileUsingAccessToken(authResponse.AccessToken!) - .ConfigureAwait(false); + PlayerProfile profile = await mojang.GetProfileUsingAccessToken(authResponse.AccessToken!); Session session = new Session { Username = profile.Name, diff --git a/MojangAPI/MojangException.cs b/MojangAPI/MojangException.cs index 9de5bf6..34d1540 100644 --- a/MojangAPI/MojangException.cs +++ b/MojangAPI/MojangException.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using System.Net.Http; @@ -69,8 +69,7 @@ public static MojangException ParseFromResponse(string responseBody, int statusC { try { - var resBody = await response.Content.ReadAsStringAsync() - .ConfigureAwait(false); + var resBody = await response.Content.ReadAsStringAsync(); if (!string.IsNullOrEmpty(resBody)) ex = ParseFromResponse(resBody, (int)response.StatusCode); } @@ -89,8 +88,7 @@ public static MojangException ParseFromResponse(string responseBody, int statusC public static Func> GetMojangErrorHandler() => async (response, ex) => { - await GetMojangErrorHandler().Invoke(response, ex) - .ConfigureAwait(false); + await GetMojangErrorHandler().Invoke(response, ex); return default(T); }; } diff --git a/MojangAPI/SecurityQuestion/QuestionFlow.cs b/MojangAPI/SecurityQuestion/QuestionFlow.cs index f4eba9c..158c338 100644 --- a/MojangAPI/SecurityQuestion/QuestionFlow.cs +++ b/MojangAPI/SecurityQuestion/QuestionFlow.cs @@ -55,8 +55,7 @@ public Task GetQuestionList(string accessToken) => ResponseHandler = async (response) => { - string content = await response.Content.ReadAsStringAsync() - .ConfigureAwait(false); + string content = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(content); List questions = new List(3); From 6bba5851b8541b2223d30e5b332c3ae05cc6a71c Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Mon, 2 Nov 2026 01:49:05 +0900 Subject: [PATCH 07/12] Require JSON metadata in JsonHttpContent constructors --- MojangAPI/HttpAction/JsonHttpContent.cs | 59 +++++++------------------ MojangAPI/Mojang.cs | 6 +-- MojangAPI/MojangAPI.csproj | 2 +- MojangAPI/MojangAuth.cs | 10 ++--- 4 files changed, 25 insertions(+), 52 deletions(-) diff --git a/MojangAPI/HttpAction/JsonHttpContent.cs b/MojangAPI/HttpAction/JsonHttpContent.cs index d06ec5c..4ad53d3 100644 --- a/MojangAPI/HttpAction/JsonHttpContent.cs +++ b/MojangAPI/HttpAction/JsonHttpContent.cs @@ -2,9 +2,6 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization.Metadata; -#if NET10_0_OR_GREATER -using System.Diagnostics.CodeAnalysis; -#endif namespace HttpAction { @@ -12,56 +9,32 @@ public class JsonHttpContent : StringContent { private const string DefaultMediaType = "application/json"; -#if NET10_0_OR_GREATER - [RequiresUnreferencedCode("Use Create with source-generated JSON metadata.")] - [RequiresDynamicCode("Use Create with source-generated JSON metadata.")] -#endif - public JsonHttpContent(object obj) - : this(obj, null) + public JsonHttpContent(object? obj, JsonTypeInfo typeInfo) + : this(obj, typeInfo, null) { - - } - -#if NET10_0_OR_GREATER - [RequiresUnreferencedCode("Use Create with source-generated JSON metadata.")] - [RequiresDynamicCode("Use Create with source-generated JSON metadata.")] -#endif - public JsonHttpContent(object obj, Encoding? encoding) - : this(obj, encoding, DefaultMediaType) - { - - } - -#if NET10_0_OR_GREATER - [RequiresUnreferencedCode("Use Create with source-generated JSON metadata.")] - [RequiresDynamicCode("Use Create with source-generated JSON metadata.")] -#endif - public JsonHttpContent(object obj, Encoding? encoding, string mediaType) - : base(serialize(obj), encoding, mediaType) - { - } - private JsonHttpContent(string json, Encoding? encoding, string mediaType) - : base(json, encoding, mediaType) + public JsonHttpContent(object? obj, JsonTypeInfo typeInfo, Encoding? encoding) + : this( + obj, + typeInfo, + encoding, + DefaultMediaType) { } - public static JsonHttpContent Create(T value, JsonTypeInfo typeInfo) + public JsonHttpContent( + object? obj, + JsonTypeInfo typeInfo, + Encoding? encoding, + string mediaType) + : base(serialize(obj, typeInfo), encoding, mediaType) { - return new JsonHttpContent(JsonSerializer.Serialize(value, typeInfo), Encoding.UTF8, DefaultMediaType); } -#if NET10_0_OR_GREATER - [RequiresUnreferencedCode("Use Create with source-generated JSON metadata.")] - [RequiresDynamicCode("Use Create with source-generated JSON metadata.")] -#endif - public static string serialize(object obj) + public static string serialize(object? obj, JsonTypeInfo typeInfo) { - return JsonSerializer.Serialize(obj, new JsonSerializerOptions - { - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull - }); + return JsonSerializer.Serialize(obj, typeInfo); } } } diff --git a/MojangAPI/Mojang.cs b/MojangAPI/Mojang.cs index c2c6a9c..9865b73 100644 --- a/MojangAPI/Mojang.cs +++ b/MojangAPI/Mojang.cs @@ -74,7 +74,7 @@ public Task GetUUIDs(string[] usernames) => Method = HttpMethod.Post, Host = "https://api.mojang.com", Path = "profiles/minecraft", - Content = JsonHttpContent.Create(usernames ?? throw new ArgumentNullException(), MojangJson.GetTypeInfo()), + Content = new JsonHttpContent(usernames ?? throw new ArgumentNullException(), MojangJson.GetTypeInfo()), ResponseHandler = HttpResponseHandlers.GetJsonArrayHandler(), ErrorHandler = MojangException.GetMojangErrorHandler() }); @@ -283,7 +283,7 @@ public Task ChangeSkin(string uuid, string accessToken, SkinType { "Authorization", "Bearer " + accessToken } }, - Content = JsonHttpContent.Create(new SkinRequest + Content = new JsonHttpContent(new SkinRequest { Variant = skinType.GetModelType(), Url = skinUrl @@ -400,7 +400,7 @@ public Task GetStatistics(string[] options) => Host = "https://api.mojang.com", Path = "orders/statistics", - Content = JsonHttpContent.Create(new StatisticsRequest + Content = new JsonHttpContent(new StatisticsRequest { MetricKeys = options }, MojangJson.GetTypeInfo()), diff --git a/MojangAPI/MojangAPI.csproj b/MojangAPI/MojangAPI.csproj index 64f50a8..757d486 100644 --- a/MojangAPI/MojangAPI.csproj +++ b/MojangAPI/MojangAPI.csproj @@ -22,6 +22,6 @@ - + diff --git a/MojangAPI/MojangAuth.cs b/MojangAPI/MojangAuth.cs index 6db3c61..0d0d868 100644 --- a/MojangAPI/MojangAuth.cs +++ b/MojangAPI/MojangAuth.cs @@ -102,7 +102,7 @@ public Task Authenticate(string email, string password, stri Method = HttpMethod.Post, Path = "authenticate", - Content = JsonHttpContent.Create(new AuthenticationRequest + Content = new JsonHttpContent(new AuthenticationRequest { Username = email, Password = password, @@ -137,7 +137,7 @@ public Task Refresh(Session session) => Method = HttpMethod.Post, Path = "refresh", - Content = JsonHttpContent.Create(new TokenRequest + Content = new JsonHttpContent(new TokenRequest { AccessToken = session.AccessToken, ClientToken = session.ClientToken, @@ -176,7 +176,7 @@ public Task Validate(string accessToken, string? clientToken Method = HttpMethod.Post, Path = "validate", - Content = JsonHttpContent.Create(new TokenRequest + Content = new JsonHttpContent(new TokenRequest { AccessToken = accessToken, ClientToken = clientToken @@ -198,7 +198,7 @@ public Task Signout(string email, string password) => Method = HttpMethod.Post, Path = "signout", - Content = JsonHttpContent.Create(new CredentialsRequest + Content = new JsonHttpContent(new CredentialsRequest { Username = email, Password = password @@ -221,7 +221,7 @@ public Task Invalidate(string accessToken, string? clientTok Method = HttpMethod.Post, Path = "invalidate", - Content = JsonHttpContent.Create(new TokenRequest + Content = new JsonHttpContent(new TokenRequest { AccessToken = accessToken, ClientToken = clientToken From 0ba71a99b3c99e4a9a4b4cf3f33200a603885b27 Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Mon, 2 Nov 2026 02:17:31 +0900 Subject: [PATCH 08/12] Require generated JSON metadata on all target frameworks --- MojangAPI/HttpAction/HttpResponseHandlers.cs | 44 ++++++++++++-------- MojangAPI/Mojang.cs | 10 ++--- MojangAPI/SecurityQuestion/QuestionFlow.cs | 3 +- MojangAPI/Serialization/MojangJson.cs | 19 --------- 4 files changed, 34 insertions(+), 42 deletions(-) diff --git a/MojangAPI/HttpAction/HttpResponseHandlers.cs b/MojangAPI/HttpAction/HttpResponseHandlers.cs index f64d2a0..f926dda 100644 --- a/MojangAPI/HttpAction/HttpResponseHandlers.cs +++ b/MojangAPI/HttpAction/HttpResponseHandlers.cs @@ -22,7 +22,7 @@ public static Func> GetDefaultResponseHandler() if (typeof(T) == typeof(Stream)) return GetStreamResponseHandler() as Func>; - return GetJsonHandler(); + return response => GetJsonHandler(MojangJson.GetTypeInfo())(response); } public static Func> GetSuccessCodeResponseHandler() => @@ -65,15 +65,16 @@ public static Func> GetSuccessCodeResponseHandler> GetJsonErrorHandler() where T : new() => GetJsonErrorHandler(new T()); + public static Func> GetJsonErrorHandler(JsonTypeInfo typeInfo) where T : new() => + GetJsonErrorHandler(new T(), typeInfo); - public static Func> GetJsonErrorHandler(T defaultObj) => - GetJsonErrorHandler(defaultObj, null); + public static Func> GetJsonErrorHandler(T defaultObj, JsonTypeInfo typeInfo) + { + if (typeInfo == null) + throw new ArgumentNullException(nameof(typeInfo)); - public static Func> GetJsonErrorHandler(T defaultObj, JsonTypeInfo? typeInfo) => - async (response, ex) => + return async (response, ex) => { - // if (ex == null) { try @@ -81,33 +82,39 @@ public static Func> GetSuccessCodeResponseHandler(resBody, typeInfo); + var obj = JsonSerializer.Deserialize(resBody, typeInfo); if (obj != null) return obj; } } catch { - // ignore exception to execute next error handler + // 응답을 해석하지 못하면 HTTP 상태와 원래 예외로 실패를 전달한다. } } response.EnsureSuccessStatusCode(); if (ex != null) throw ex; + return defaultObj; }; + } - public static Func> GetJsonHandler() => GetJsonHandler(null); + public static Func> GetJsonHandler(JsonTypeInfo typeInfo) + { + if (typeInfo == null) + throw new ArgumentNullException(nameof(typeInfo)); - public static Func> GetJsonHandler(JsonTypeInfo? typeInfo) => - async (response) => + return async response => { string res = await response.Content.ReadAsStringAsync(); if (string.IsNullOrEmpty(res)) res = "{}"; - return MojangJson.Deserialize(res, typeInfo); + + return JsonSerializer.Deserialize(res, typeInfo); }; + } public static Func> GetDefaultErrorHandler() => (response, ex) => @@ -119,10 +126,12 @@ public static Func> GetJsonHandler(JsonTypeInfo< return Task.FromResult(default(T)); }; - public static Func> GetJsonArrayHandler() => GetJsonArrayHandler(null); + public static Func> GetJsonArrayHandler(JsonTypeInfo typeInfo) + { + if (typeInfo == null) + throw new ArgumentNullException(nameof(typeInfo)); - public static Func> GetJsonArrayHandler(JsonTypeInfo? typeInfo) => - async (response) => + return async response => { string res = await response.Content.ReadAsStringAsync(); if (string.IsNullOrEmpty(res)) @@ -132,8 +141,9 @@ public static Func> GetJsonArrayHandler(JsonTy var root = doc.RootElement; return root.EnumerateArray() - .Select(elem => MojangJson.Deserialize(elem.GetRawText(), typeInfo)) + .Select(elem => JsonSerializer.Deserialize(elem.GetRawText(), typeInfo)) .ToArray(); }; + } } } diff --git a/MojangAPI/Mojang.cs b/MojangAPI/Mojang.cs index 9865b73..c49a301 100644 --- a/MojangAPI/Mojang.cs +++ b/MojangAPI/Mojang.cs @@ -62,7 +62,7 @@ public Task GetNameHistories(string uuid) => Path = $"user/profiles/{uuid?.Replace("-", "") ?? throw new ArgumentNullException(nameof(uuid))}/names", ResponseHandler = async (response) => { - var handler = HttpResponseHandlers.GetJsonArrayHandler(); + var handler = HttpResponseHandlers.GetJsonArrayHandler(MojangJson.GetTypeInfo()); return await handler.Invoke(response); }, ErrorHandler = MojangException.GetMojangErrorHandler() @@ -75,7 +75,7 @@ public Task GetUUIDs(string[] usernames) => Host = "https://api.mojang.com", Path = "profiles/minecraft", Content = new JsonHttpContent(usernames ?? throw new ArgumentNullException(), MojangJson.GetTypeInfo()), - ResponseHandler = HttpResponseHandlers.GetJsonArrayHandler(), + ResponseHandler = HttpResponseHandlers.GetJsonArrayHandler(MojangJson.GetTypeInfo()), ErrorHandler = MojangException.GetMojangErrorHandler() }); @@ -182,7 +182,7 @@ public Task GetPlayerAttributes(string accessToken) => { "Authorization", "Bearer " + accessToken } }, - ResponseHandler = HttpResponseHandlers.GetJsonHandler(), + ResponseHandler = HttpResponseHandlers.GetJsonHandler(MojangJson.GetTypeInfo()), ErrorHandler = MojangException.GetMojangErrorHandler() }); @@ -223,7 +223,7 @@ public Task GetPlayerCertificates(string accessToken) => { "Authorization", "Bearer " + accessToken } }, - ResponseHandler = HttpResponseHandlers.GetJsonHandler(), + ResponseHandler = HttpResponseHandlers.GetJsonHandler(MojangJson.GetTypeInfo()), ErrorHandler = MojangException.GetMojangErrorHandler() }); @@ -405,7 +405,7 @@ public Task GetStatistics(string[] options) => MetricKeys = options }, MojangJson.GetTypeInfo()), - ResponseHandler = HttpResponseHandlers.GetJsonHandler(), + ResponseHandler = HttpResponseHandlers.GetJsonHandler(MojangJson.GetTypeInfo()), ErrorHandler = MojangException.GetMojangErrorHandler() }); diff --git a/MojangAPI/SecurityQuestion/QuestionFlow.cs b/MojangAPI/SecurityQuestion/QuestionFlow.cs index 158c338..91d8f1b 100644 --- a/MojangAPI/SecurityQuestion/QuestionFlow.cs +++ b/MojangAPI/SecurityQuestion/QuestionFlow.cs @@ -1,3 +1,4 @@ +using MojangAPI.Serialization; using MojangAPI.Model; using System; using System.Collections.Generic; @@ -107,7 +108,7 @@ public Task SendAnswers(QuestionList list, string accessToken) Content = new StringContent(jarr.ToJsonString(), Encoding.UTF8, "application/json"), ResponseHandler = HttpResponseHandlers.GetSuccessCodeResponseHandler(), - ErrorHandler = HttpResponseHandlers.GetJsonErrorHandler() + ErrorHandler = HttpResponseHandlers.GetJsonErrorHandler(MojangJson.GetTypeInfo()) }); } } diff --git a/MojangAPI/Serialization/MojangJson.cs b/MojangAPI/Serialization/MojangJson.cs index 50c76d7..f1d5ef6 100644 --- a/MojangAPI/Serialization/MojangJson.cs +++ b/MojangAPI/Serialization/MojangJson.cs @@ -1,29 +1,10 @@ using System; -using System.Text.Json; -using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; namespace MojangAPI.Serialization { internal static class MojangJson { - internal static readonly JsonSerializerOptions Options = new JsonSerializerOptions - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - - internal static T? Deserialize(string json, JsonTypeInfo? typeInfo = null) - { -#if NET10_0_OR_GREATER - return JsonSerializer.Deserialize(json, typeInfo ?? GetTypeInfo()); -#else - if (typeInfo != null) - return JsonSerializer.Deserialize(json, typeInfo); - - return JsonSerializer.Deserialize(json, Options); -#endif - } - internal static JsonTypeInfo GetTypeInfo() { // 호출자가 정의한 형식은 생성된 메타데이터를 직접 전달해야 한다. From 8f7fe960a818af5c796839d6bc8682432726c181 Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Mon, 2 Nov 2026 03:17:42 +0900 Subject: [PATCH 09/12] Use English comments and authentication documentation --- MojangAPI/HttpAction/HttpResponseHandlers.cs | 2 +- MojangAPI/Serialization/MojangJson.cs | 2 +- docs/MojangAuth.md | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/MojangAPI/HttpAction/HttpResponseHandlers.cs b/MojangAPI/HttpAction/HttpResponseHandlers.cs index f926dda..7202d48 100644 --- a/MojangAPI/HttpAction/HttpResponseHandlers.cs +++ b/MojangAPI/HttpAction/HttpResponseHandlers.cs @@ -89,7 +89,7 @@ public static Func> GetSuccessCodeResponseHandler GetTypeInfo() { - // 호출자가 정의한 형식은 생성된 메타데이터를 직접 전달해야 한다. + // Callers must supply source-generated metadata for custom types. var typeInfo = MojangJsonContext.Default.GetTypeInfo(typeof(T)) as JsonTypeInfo; if (typeInfo == null) throw new NotSupportedException($"Supply a source-generated JsonTypeInfo<{typeof(T).Name}> for custom JSON types."); diff --git a/docs/MojangAuth.md b/docs/MojangAuth.md index 832f1a4..bfdbdeb 100644 --- a/docs/MojangAuth.md +++ b/docs/MojangAuth.md @@ -15,7 +15,7 @@ You can check whether the request was successful or failed to check `IsSuccess` `Session` property is the result of authenticate. It contains `Username`, `AccessToken`, and `UUID` If `IsSuccess` is false, `Result`, `Error`, and `ErrorMessage` property tell you why the request failed. -`MojangAuth`는 세션을 저장하지 않습니다. 인증 결과의 `Session`은 호출자가 보관하고, 재인증 시 `TryAutoLogin(session)`, `Refresh(session)`, `Validate(session)`에 직접 전달해야 합니다. +`MojangAuth` does not persist sessions. Callers must keep the returned `Session` and pass it explicitly to `TryAutoLogin(session)`, `Refresh(session)`, or `Validate(session)`. There are two ways to login. One is `Yggdrasil Mojang Authentication` which is described below, and the other is `Microsoft Xbox Authentication` which is new way to login. [Microsoft Xbox Authentication](./XboxAuthentication.md) @@ -40,17 +40,17 @@ Session session = res.Session; #### public MojangAuth(HttpClient client) -지정한 `HttpClient`로 인증 요청을 전송합니다. `null`을 전달하면 기본 클라이언트를 사용합니다. 세션 파일은 생성하거나 읽지 않습니다. +Sends authentication requests using the supplied `HttpClient`, or the default client if `null` is supplied. No session files are created or read. ### Methods #### Authenticate ```csharp -// 호출마다 새로운 클라이언트 토큰을 생성한다. +// Generate a new client token for each authentication request. MojangAuthResponse res = await auth.Authenticate("mojang email", "password"); -// 지정한 클라이언트 토큰을 사용한다. +// Use the supplied client token. MojangAuthResponse res = await auth.Authenticate("mojang email", "password", "clientToken"); if (res.IsSuccess) @@ -68,21 +68,21 @@ else #### TryAutoLogin ```csharp -// 전달한 세션을 검증하고, 검증에 실패하면 갱신한다. +// Validate the supplied session and refresh it if validation fails. MojangAuthResponse res = await auth.TryAutoLogin(session); ``` #### Refresh ```csharp -// 호출자가 보관한 세션 또는 토큰을 전달한다. +// Pass the session or tokens managed by the caller. MojangAuthResponse res = await auth.Refresh(session); ``` #### Validate ```csharp -// 호출자가 보관한 세션 또는 토큰을 전달한다. +// Pass the session or tokens managed by the caller. MojangAuthResponse res = await auth.Validate(session); ``` @@ -95,7 +95,7 @@ MojangAuthResponse res = await auth.Signout("mojang email", "password"); #### Invalidate ```csharp -// 호출자가 보관한 세션 또는 토큰을 전달한다. +// Pass the session or tokens managed by the caller. MojangAuthResponse res = await auth.Invalidate("accessToken", "clientToken"); ``` *note: 'clientToken' can be null* From 919a6d01e5e46679f55266b6ac20857d3e2000a1 Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Mon, 2 Nov 2026 03:24:16 +0900 Subject: [PATCH 10/12] Resolve nullable warnings in response and profile handling --- MojangAPI/HttpAction/HttpResponseHandlers.cs | 17 +++++++++------- MojangAPI/Mojang.cs | 21 ++++++++++---------- MojangAPI/MojangException.cs | 12 ++++------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/MojangAPI/HttpAction/HttpResponseHandlers.cs b/MojangAPI/HttpAction/HttpResponseHandlers.cs index 7202d48..816042a 100644 --- a/MojangAPI/HttpAction/HttpResponseHandlers.cs +++ b/MojangAPI/HttpAction/HttpResponseHandlers.cs @@ -13,14 +13,15 @@ public static class HttpResponseHandlers { public static Func> GetDefaultResponseHandler() { + // Each type check guarantees that the delegate result type matches T. if (typeof(T) == typeof(bool)) - return GetSuccessCodeResponseHandler() as Func>; + return (Func>)(object)GetSuccessCodeResponseHandler(); if (typeof(T) == typeof(int)) - return GetStatusCodeResponseHandler() as Func>; + return (Func>)(object)GetStatusCodeResponseHandler(); if (typeof(T) == typeof(string)) - return GetStringResponseHandler() as Func>; + return (Func>)(object)GetStringResponseHandler(); if (typeof(T) == typeof(Stream)) - return GetStreamResponseHandler() as Func>; + return (Func>)(object)GetStreamResponseHandler(); return response => GetJsonHandler(MojangJson.GetTypeInfo())(response); } @@ -112,11 +113,12 @@ public static Func> GetJsonHandler(JsonTypeInfo< if (string.IsNullOrEmpty(res)) res = "{}"; - return JsonSerializer.Deserialize(res, typeInfo); + return JsonSerializer.Deserialize(res, typeInfo) + ?? throw new JsonException("The response must contain a non-null JSON value."); }; } - public static Func> GetDefaultErrorHandler() => + public static Func> GetDefaultErrorHandler() => (response, ex) => { response.EnsureSuccessStatusCode(); @@ -141,7 +143,8 @@ public static Func> GetJsonArrayHandler(JsonTy var root = doc.RootElement; return root.EnumerateArray() - .Select(elem => JsonSerializer.Deserialize(elem.GetRawText(), typeInfo)) + .Select(elem => JsonSerializer.Deserialize(elem.GetRawText(), typeInfo) + ?? throw new JsonException("The response array must not contain null elements.")) .ToArray(); }; } diff --git a/MojangAPI/Mojang.cs b/MojangAPI/Mojang.cs index c49a301..c58a947 100644 --- a/MojangAPI/Mojang.cs +++ b/MojangAPI/Mojang.cs @@ -100,10 +100,11 @@ private async Task profileResponseHandlerFromUUID(HttpResponseMes try { var innerValue = root.GetProperty("properties") - .EnumerateArray() - .First() - .GetProperty("value") - .GetString(); + .EnumerateArray() + .First() + .GetProperty("value") + .GetString() + ?? throw new JsonException("The profile texture value must be a non-null string."); var decoded = Convert.FromBase64String(innerValue); using var skinDocument = JsonDocument.Parse(decoded); @@ -118,10 +119,10 @@ private async Task profileResponseHandlerFromUUID(HttpResponseMes catch { SkinType? defaultSkinType = null; - if (string.IsNullOrEmpty(profile?.UUID)) - defaultSkinType = Skin.GetDefaultSkinType(profile!.UUID!); + if (profile.UUID is string profileUuid && profileUuid.Length == 32) + defaultSkinType = Skin.GetDefaultSkinType(profileUuid); - profile!.Skin = new Skin(null, defaultSkinType); + profile.Skin = new Skin(null, defaultSkinType); } return profile; @@ -161,10 +162,10 @@ private async Task profileResponseHandlerFromAccessToken(HttpResp catch { SkinType? defaultSkinType = null; - if (string.IsNullOrEmpty(profile?.UUID)) - defaultSkinType = Skin.GetDefaultSkinType(profile!.UUID!); + if (profile.UUID is string profileUuid && profileUuid.Length == 32) + defaultSkinType = Skin.GetDefaultSkinType(profileUuid); - profile!.Skin = new Skin(null, defaultSkinType); + profile.Skin = new Skin(null, defaultSkinType); } return profile; diff --git a/MojangAPI/MojangException.cs b/MojangAPI/MojangException.cs index 34d1540..a50952a 100644 --- a/MojangAPI/MojangException.cs +++ b/MojangAPI/MojangException.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using System.Net.Http; @@ -62,7 +62,7 @@ public static MojangException ParseFromResponse(string responseBody, int statusC } } - public static Func GetMojangErrorHandler() => + public static Func> GetMojangErrorHandler() => async (response, ex) => { if (ex == null) @@ -85,11 +85,7 @@ public static MojangException ParseFromResponse(string responseBody, int statusC throw new MojangException($"{(int)response.StatusCode}: {response.ReasonPhrase}", (int)response.StatusCode); }; - public static Func> GetMojangErrorHandler() => - async (response, ex) => - { - await GetMojangErrorHandler().Invoke(response, ex); - return default(T); - }; + public static Func GetMojangErrorHandler() => + (response, ex) => GetMojangErrorHandler()(response, ex); } } From 17a7f34c21844a574427f35dc697672e389aca78 Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Mon, 2 Nov 2026 03:27:50 +0900 Subject: [PATCH 11/12] Use type patterns to select default response handlers --- MojangAPI/HttpAction/HttpResponseHandlers.cs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/MojangAPI/HttpAction/HttpResponseHandlers.cs b/MojangAPI/HttpAction/HttpResponseHandlers.cs index 816042a..d9352da 100644 --- a/MojangAPI/HttpAction/HttpResponseHandlers.cs +++ b/MojangAPI/HttpAction/HttpResponseHandlers.cs @@ -13,15 +13,17 @@ public static class HttpResponseHandlers { public static Func> GetDefaultResponseHandler() { - // Each type check guarantees that the delegate result type matches T. - if (typeof(T) == typeof(bool)) - return (Func>)(object)GetSuccessCodeResponseHandler(); - if (typeof(T) == typeof(int)) - return (Func>)(object)GetStatusCodeResponseHandler(); - if (typeof(T) == typeof(string)) - return (Func>)(object)GetStringResponseHandler(); - if (typeof(T) == typeof(Stream)) - return (Func>)(object)GetStreamResponseHandler(); + if (GetSuccessCodeResponseHandler() is Func> successHandler) + return successHandler; + + if (GetStatusCodeResponseHandler() is Func> statusCodeHandler) + return statusCodeHandler; + + if (GetStringResponseHandler() is Func> stringHandler) + return stringHandler; + + if (GetStreamResponseHandler() is Func> streamHandler) + return streamHandler; return response => GetJsonHandler(MojangJson.GetTypeInfo())(response); } From 59668bbdda780209426577b510016b7613e42d4d Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Mon, 2 Nov 2026 03:29:56 +0900 Subject: [PATCH 12/12] Remove UUID length checks from skin fallback --- MojangAPI/Mojang.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MojangAPI/Mojang.cs b/MojangAPI/Mojang.cs index c58a947..a4dc3a8 100644 --- a/MojangAPI/Mojang.cs +++ b/MojangAPI/Mojang.cs @@ -119,7 +119,7 @@ private async Task profileResponseHandlerFromUUID(HttpResponseMes catch { SkinType? defaultSkinType = null; - if (profile.UUID is string profileUuid && profileUuid.Length == 32) + if (profile.UUID is string profileUuid && !string.IsNullOrEmpty(profileUuid)) defaultSkinType = Skin.GetDefaultSkinType(profileUuid); profile.Skin = new Skin(null, defaultSkinType); @@ -162,7 +162,7 @@ private async Task profileResponseHandlerFromAccessToken(HttpResp catch { SkinType? defaultSkinType = null; - if (profile.UUID is string profileUuid && profileUuid.Length == 32) + if (profile.UUID is string profileUuid && !string.IsNullOrEmpty(profileUuid)) defaultSkinType = Skin.GetDefaultSkinType(profileUuid); profile.Skin = new Skin(null, defaultSkinType);